""" 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 pytest from PyQt5.QtCore import QEvent, Qt, QThreadPool from PyQt5.QtGui import QClipboard, QFont, QMouseEvent, QTextBlock, QTextCursor, QTextOption from PyQt5.QtWidgets import QAction, QApplication, QMenu from novelwriter import CONFIG, SHARED from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import ( nwDocAction, nwDocInsert, nwItemClass, nwItemLayout, nwTrinary, nwWidget ) from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.text.counting import standardCounter from novelwriter.types import ( QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtMouseLeft, QtMoveRight ) from tests.mocked import causeOSError from tests.tools import C, buildTestProject KEY_DELAY = 1 def getMenuForPos(editor: GuiDocEditor, pos: int, select: bool = False) -> QMenu | None: """Create a context menu for a text position and return the menu object. """ cursor = editor.textCursor() cursor.setPosition(pos) if select: cursor.select(QTextCursor.SelectionType.WordUnderCursor) editor.setTextCursor(cursor) editor._openContextMenu(editor.cursorRect().center()) for obj in editor.children(): if isinstance(obj, QMenu) and obj.objectName() == "ContextMenu": return obj return None @pytest.mark.gui def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test initialising the editor.""" # Open project buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) docEditor = nwGUI.docEditor docEditor.setPlainText("### Lorem Ipsum\n\n%s" % ipsumText[0]) nwGUI.saveDocument() # Check Defaults qDoc = docEditor.document() assert qDoc.defaultTextOption().alignment() == QtAlignLeft assert docEditor.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert docEditor.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert docEditor._typPadChar == nwUnicode.U_NBSP assert docEditor.docHeader.itemTitle.text() == ( "Novel \u203a New Chapter \u203a New Scene" ) assert docEditor.docHeader._docOutline == {0: "### New Scene"} # Check that editor handles settings CONFIG.textFont = QFont() CONFIG.doJustify = True CONFIG.showTabsNSpaces = True CONFIG.showLineEndings = True CONFIG.hideVScroll = True CONFIG.hideHScroll = True CONFIG.fmtPadThin = True CONFIG.showFullPath = False docEditor.initEditor() qDoc = docEditor.document() assert CONFIG.textFont == qDoc.defaultFont() assert qDoc.defaultTextOption().alignment() == QtAlignJustify assert qDoc.defaultTextOption().flags() & QTextOption.ShowTabsAndSpaces assert qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators assert docEditor.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert docEditor.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert docEditor._typPadChar == nwUnicode.U_THNBSP assert docEditor.docHeader.itemTitle.text() == "New Scene" # Header # ====== # Go to outline docEditor.setCursorLine(3) docEditor.docHeader.outlineMenu.actions()[0].trigger() assert docEditor.getCursorPosition() == 0 # Select item from header with qtbot.waitSignal(docEditor.requestProjectItemSelected, timeout=1000) as signal: qtbot.mouseClick(docEditor.docHeader, QtMouseLeft) assert signal.args == [docEditor.docHeader._docHandle, True] # Close from header with qtbot.waitSignal(docEditor.docHeader.closeDocumentRequest, timeout=1000): docEditor.docHeader.closeButton.click() assert docEditor.docHeader.tbButton.isVisible() is False assert docEditor.docHeader.searchButton.isVisible() is False assert docEditor.docHeader.closeButton.isVisible() is False assert docEditor.docHeader.minmaxButton.isVisible() is False # Select item from header with qtbot.waitSignal(docEditor.requestProjectItemSelected, timeout=1000) as signal: qtbot.mouseClick(docEditor.docHeader, QtMouseLeft) assert signal.args == ["", True] # qtbot.stop() @pytest.mark.gui def testGuiEditor_LoadText(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test loading text into the editor.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText*20) docEditor.replaceText(longText) nwGUI.saveDocument() nwGUI.closeDocument() # Invalid handle assert docEditor.loadText("abcdefghijklm") is False # Regular open assert docEditor.loadText(C.hSceneDoc) is True # Regular open, with line number (1 indexed) assert docEditor.loadText(C.hSceneDoc, tLine=4) is True cursPos = docEditor.getCursorPosition() assert docEditor.document().findBlock(cursPos).blockNumber() == 3 # Load empty document docEditor.replaceText("") nwGUI.saveDocument() assert docEditor.loadText(C.hSceneDoc) is True assert docEditor.toPlainText() == "" # qtbot.stop() @pytest.mark.gui def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumText, mockRnd): """Test saving text from the editor.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText) docEditor.replaceText(longText) # Missing item nwItem = docEditor._nwItem docEditor._nwItem = None assert docEditor.saveText() is False docEditor._nwItem = nwItem # Unknown handle docEditor._docHandle = "0123456789abcdef" assert docEditor.saveText() is False docEditor._docHandle = C.hSceneDoc # Cause error when saving with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert docEditor.saveText() is False assert "Could not save document." in caplog.text # Change header level assert SHARED.project.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT # type: ignore docEditor.replaceText(longText[1:]) assert docEditor.saveText() is True assert SHARED.project.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT # type: ignore # Regular save assert docEditor.saveText() is True # qtbot.stop() @pytest.mark.gui def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): """Test extracting various meta data and other values.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor # Get Text # This should replace line and paragraph separators, but preserve # non-breaking spaces. newText = ( "### New Scene\u2029\u2029" "Some\u2028text.\u2029" "More\u00a0text.\u2029" ) docEditor.replaceText(newText) assert docEditor.getText() == ( "### New Scene\n\n" "Some\n" "text.\n" "More\u00a0text.\n" ) # Check Properties assert docEditor.docChanged is True assert docEditor.docHandle == C.hSceneDoc assert docEditor.lastActive > 0.0 assert docEditor.isEmpty is False # Cursor Position docEditor.setCursorPosition(10) assert docEditor.getCursorPosition() == 10 assert SHARED.project.tree[C.hSceneDoc].cursorPos != 10 # type: ignore docEditor.saveCursorPosition() assert SHARED.project.tree[C.hSceneDoc].cursorPos == 10 # type: ignore docEditor.setCursorLine(None) assert docEditor.getCursorPosition() == 10 docEditor.setCursorLine(3) assert docEditor.getCursorPosition() == 15 # Document Changed Signal docEditor._docChanged = False with qtbot.waitSignal(docEditor.editedStatusChanged, raising=True, timeout=100): docEditor.setDocumentChanged(True) assert docEditor._docChanged is True # qtbot.stop() @pytest.mark.gui def testGuiEditor_ContextMenu(monkeypatch, qtbot, nwGUI, projPath, mockRnd): """Test the editor context menu.""" monkeypatch.setattr(QMenu, "exec", lambda *a: None) buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor sceneItem = SHARED.project.tree[C.hSceneDoc] assert sceneItem is not None docText = ( "### A Scene\n\n" "@pov: Jane\n" "Some text ..." ) docEditor.setPlainText(docText) assert docEditor.getText() == docText # Rename Item from Heading ctxMenu = getMenuForPos(docEditor, 1) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Set as Document Name", "Paste", "Select All", "Select Word", "Select Paragraph" ] with monkeypatch.context() as mp: mp.setattr(GuiEditLabel, "getLabel", lambda a, text: (text, True)) assert sceneItem.itemName == "New Scene" ctxMenu.actions()[0].trigger() assert sceneItem.itemName == "A Scene" ctxMenu.setObjectName("") ctxMenu.deleteLater() # Create Character ctxMenu = getMenuForPos(docEditor, 21) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Create Note for Tag", "Paste", "Select All", "Select Word", "Select Paragraph" ] ctxMenu.actions()[0].trigger() janeItem = SHARED.project.tree["0000000000010"] assert janeItem is not None assert janeItem.itemName == "Jane" ctxMenu.setObjectName("") ctxMenu.deleteLater() # Follow Character Tag ctxMenu = getMenuForPos(docEditor, 21) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Follow Tag", "Paste", "Select All", "Select Word", "Select Paragraph" ] ctxMenu.actions()[0].trigger() assert nwGUI.docViewer.docHandle == "0000000000010" ctxMenu.setObjectName("") ctxMenu.deleteLater() # Select Word ctxMenu = getMenuForPos(docEditor, 31) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Paste", "Select All", "Select Word", "Select Paragraph" ] ctxMenu.actions()[3].trigger() assert docEditor.textCursor().selectedText() == "text" ctxMenu.setObjectName("") ctxMenu.deleteLater() # Select Paragraph ctxMenu = getMenuForPos(docEditor, 31) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Paste", "Select All", "Select Word", "Select Paragraph" ] ctxMenu.actions()[4].trigger() assert docEditor.textCursor().selectedText() == "Some text ..." ctxMenu.setObjectName("") ctxMenu.deleteLater() # Select All ctxMenu = getMenuForPos(docEditor, 31) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Paste", "Select All", "Select Word", "Select Paragraph" ] ctxMenu.actions()[2].trigger() assert docEditor.textCursor().selectedText() == docEditor.document().toRawText() ctxMenu.setObjectName("") ctxMenu.deleteLater() # Copy Text ctxMenu = getMenuForPos(docEditor, 31, True) assert ctxMenu is not None assert docEditor.textCursor().selectedText() == "text" actions = [x.text() for x in ctxMenu.actions() if x.text()] assert actions == [ "Cut", "Copy", "Paste", "Select All", "Select Word", "Select Paragraph" ] QApplication.clipboard().clear() ctxMenu.actions()[1].trigger() assert QApplication.clipboard().text(QClipboard.Mode.Clipboard) == "text" # Cut Text QApplication.clipboard().clear() ctxMenu.actions()[0].trigger() assert QApplication.clipboard().text(QClipboard.Mode.Clipboard) == "text" assert "text" not in docEditor.getText() # Paste Text ctxMenu.actions()[2].trigger() assert docEditor.getText() == docText ctxMenu.setObjectName("") ctxMenu.deleteLater() # qtbot.stop() @pytest.mark.gui def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the document spell checker.""" monkeypatch.setattr(QMenu, "exec", lambda *a: None) buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) docEditor.replaceText(text) # Toggle State # ============ # No language set SHARED.spelling._language = None SHARED.project.data.setSpellCheck(False) docEditor.toggleSpellCheck(True) assert SHARED.project.data.spellCheck is False # No spell checker available SHARED.spelling._language = "en" docEditor.toggleSpellCheck(True) assert SHARED.project.data.spellCheck is True CONFIG.hasEnchant = False docEditor.toggleSpellCheck(True) assert SHARED.project.data.spellCheck is False CONFIG.hasEnchant = True docEditor.toggleSpellCheck(True) assert SHARED.project.data.spellCheck is True # Plain Toggle docEditor.toggleSpellCheck(None) assert SHARED.project.data.spellCheck is False # Run SpellCheck # ============== SHARED.project.data.setSpellCheck(True) # With Suggestion with monkeypatch.context() as mp: mp.setattr(docEditor._qDocument, "spellErrorAtPos", lambda *a: ("Lorem", 0, 5, ["Lorax"])) ctxMenu = getMenuForPos(docEditor, 16) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert "Spelling Suggestion(s)" in actions assert f"{nwUnicode.U_ENDASH} Lorax" in actions ctxMenu.actions()[7].trigger() QApplication.processEvents() assert docEditor.getText() == text.replace("Lorem", "Lorax", 1) ctxMenu.setObjectName("") ctxMenu.deleteLater() # Without Suggestion with monkeypatch.context() as mp: mp.setattr(docEditor._qDocument, "spellErrorAtPos", lambda *a: ("Lorax", 0, 5, [])) ctxMenu = getMenuForPos(docEditor, 16) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert f"{nwUnicode.U_ENDASH} No Suggestions" in actions assert docEditor.getText() == text.replace("Lorem", "Lorax", 1) ctxMenu.setObjectName("") ctxMenu.deleteLater() # Add to Dictionary with monkeypatch.context() as mp: mp.setattr(docEditor._qDocument, "spellErrorAtPos", lambda *a: ("Lorax", 0, 5, [])) ctxMenu = getMenuForPos(docEditor, 16) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert "Add Word to Dictionary" in actions assert "Lorax" not in SHARED.spelling._userDict ctxMenu.actions()[7].trigger() assert "Lorax" in SHARED.spelling._userDict ctxMenu.setObjectName("") ctxMenu.deleteLater() # qtbot.stop() @pytest.mark.gui def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the document actions. This is not an extensive test of the action features, just that the actions are actually called. The various action features are tested when their respective functions are tested. """ buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) docEditor.replaceText(text) doc = docEditor.document() # Select/Cut/Copy/Paste/Undo/Redo # =============================== QApplication.clipboard().clear() # Select All assert docEditor.docAction(nwDocAction.SEL_ALL) is True cursor = docEditor.textCursor() assert cursor.hasSelection() is True assert cursor.selectedText() == text.replace("\n", "\u2029") cursor.clearSelection() # Select Paragraph docEditor.setCursorPosition(1000) assert docEditor.getCursorPosition() == 1000 assert docEditor.docAction(nwDocAction.SEL_PARA) is True cursor = docEditor.textCursor() assert cursor.selectedText() == ipsumText[1] # Cut Selected Text docEditor.replaceText(text) docEditor.setCursorPosition(1000) assert docEditor.docAction(nwDocAction.SEL_PARA) is True assert docEditor.docAction(nwDocAction.CUT) is True newText = docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) assert newPara[0] == "### A Scene" assert newPara[1] == ipsumText[0] assert newPara[2] == ipsumText[2] assert newPara[3] == ipsumText[3] assert newPara[4] == ipsumText[4] # Paste Back In assert docEditor.docAction(nwDocAction.PASTE) is True assert docEditor.getText() == text # Copy Next Paragraph docEditor.replaceText(text) docEditor.setCursorPosition(1500) assert docEditor.docAction(nwDocAction.SEL_PARA) is True assert docEditor.docAction(nwDocAction.COPY) is True # Paste at End docEditor.setCursorPosition(doc.characterCount()) cursor = docEditor.textCursor() cursor.insertBlock() cursor.insertBlock() assert docEditor.docAction(nwDocAction.PASTE) is True newText = docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) assert newPara[5] == ipsumText[4] assert newPara[6] == ipsumText[2] QApplication.clipboard().clear() # Emphasis/Undo/Redo # ================== text = "### A Scene\n\n%s" % ipsumText[0] docEditor.replaceText(text) # Emphasis docEditor.setCursorPosition(50) assert docEditor.docAction(nwDocAction.MD_ITALIC) is True assert docEditor.getText() == text.replace("consectetur", "_consectetur_") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Strong docEditor.setCursorPosition(50) assert docEditor.docAction(nwDocAction.MD_BOLD) is True assert docEditor.getText() == text.replace("consectetur", "**consectetur**") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Strikeout docEditor.setCursorPosition(50) assert docEditor.docAction(nwDocAction.MD_STRIKE) is True assert docEditor.getText() == text.replace("consectetur", "~~consectetur~~") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Redo assert docEditor.docAction(nwDocAction.REDO) is True assert docEditor.getText() == text.replace("consectetur", "~~consectetur~~") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Shortcodes # ========== text = "### A Scene\n\n%s" % ipsumText[0] docEditor.replaceText(text) # Italic docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_ITALIC) is True assert docEditor.getText() == text.replace("consectetur", "[i]consectetur[/i]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Bold docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_BOLD) is True assert docEditor.getText() == text.replace("consectetur", "[b]consectetur[/b]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Strikethrough docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_STRIKE) is True assert docEditor.getText() == text.replace("consectetur", "[s]consectetur[/s]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Underline docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_ULINE) is True assert docEditor.getText() == text.replace("consectetur", "[u]consectetur[/u]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Mark docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_MARK) is True assert docEditor.getText() == text.replace("consectetur", "[m]consectetur[/m]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Superscript docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_SUP) is True assert docEditor.getText() == text.replace("consectetur", "[sup]consectetur[/sup]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Subscript docEditor.setCursorPosition(46) assert docEditor.docAction(nwDocAction.SC_SUB) is True assert docEditor.getText() == text.replace("consectetur", "[sub]consectetur[/sub]") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Quotes # ====== text = "### A Scene\n\n%s" % ipsumText[0] docEditor.replaceText(text) # Add Single Quotes docEditor.setCursorPosition(50) assert docEditor.docAction(nwDocAction.S_QUOTE) is True assert docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Add Double Quotes docEditor.setCursorPosition(50) assert docEditor.docAction(nwDocAction.D_QUOTE) is True assert docEditor.getText() == text.replace("consectetur", "\u201cconsectetur\u201d") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text # Replace Single Quotes repText = text.replace("consectetur", "'consectetur'") docEditor.replaceText(repText) assert docEditor.docAction(nwDocAction.SEL_ALL) is True assert docEditor.docAction(nwDocAction.REPL_SNG) is True assert docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") # Replace Double Quotes repText = text.replace("consectetur", "\"consectetur\"") docEditor.replaceText(repText) assert docEditor.docAction(nwDocAction.SEL_ALL) is True assert docEditor.docAction(nwDocAction.REPL_DBL) is True assert docEditor.getText() == text.replace("consectetur", "\u201cconsectetur\u201d") # Remove Line Breaks # ================== text = "### A Scene\n\n%s" % ipsumText[0] repText = text[:100] + text[100:].replace(" ", "\n", 3) docEditor.replaceText(repText) assert docEditor.docAction(nwDocAction.RM_BREAKS) is True assert docEditor.getText().strip() == text.strip() # Format Block # ============ text = "## Scene Title\n\nScene text.\n\n" docEditor.replaceText(text) # Header 1 docEditor.setCursorPosition(0) assert docEditor.docAction(nwDocAction.BLOCK_H1) is True assert docEditor.getText() == "# Scene Title\n\nScene text.\n\n" # Header 2 docEditor.setCursorPosition(0) assert docEditor.docAction(nwDocAction.BLOCK_H2) is True assert docEditor.getText() == "## Scene Title\n\nScene text.\n\n" # Header 3 docEditor.setCursorPosition(0) assert docEditor.docAction(nwDocAction.BLOCK_H3) is True assert docEditor.getText() == "### Scene Title\n\nScene text.\n\n" # Header 4 docEditor.setCursorPosition(0) assert docEditor.docAction(nwDocAction.BLOCK_H4) is True assert docEditor.getText() == "#### Scene Title\n\nScene text.\n\n" # Comment docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.BLOCK_COM) is True assert docEditor.getText() == "#### Scene Title\n\n% Scene text.\n\n" # Ignore Text docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.BLOCK_IGN) is True assert docEditor.getText() == "#### Scene Title\n\n%~ Scene text.\n\n" # Text docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "#### Scene Title\n\nScene text.\n\n" # Align Left docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.ALIGN_L) is True assert docEditor.getText() == "#### Scene Title\n\nScene text. <<\n\n" # Align Right docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.ALIGN_R) is True assert docEditor.getText() == "#### Scene Title\n\n>> Scene text.\n\n" # Align Centre docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.ALIGN_C) is True assert docEditor.getText() == "#### Scene Title\n\n>> Scene text. <<\n\n" # Indent Left docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.INDENT_L) is True assert docEditor.getText() == "#### Scene Title\n\n> Scene text.\n\n" # Indent Right docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.INDENT_R) is True assert docEditor.getText() == "#### Scene Title\n\n> Scene text. <\n\n" # Text (Reset) docEditor.setCursorPosition(20) assert docEditor.docAction(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "#### Scene Title\n\nScene text.\n\n" # Invalid Actions # =============== # No Document Handle docEditor._docHandle = None assert docEditor.docAction(nwDocAction.BLOCK_TXT) is False docEditor._docHandle = C.hSceneDoc # Wrong Action Type assert docEditor.docAction(None) is False # Unknown Action assert docEditor.docAction(nwDocAction.NO_ACTION) is False # qtbot.stop() @pytest.mark.gui def testGuiEditor_ToolBar(qtbot, nwGUI, projPath, mockRnd): """Test the document actions. This is not an extensive test of the action features, just that the actions are actually called. The various action features are tested when their respective functions are tested. """ buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor docToolBar = docEditor.docToolBar text = ( "### A Scene\n\n" "Text bold one\n\n" "Text bold two\n\n" "Text italic one\n\n" "Text italic two\n\n" "Text strikethrough one\n\n" "Text strikethrough two\n\n" "Text underline one\n\n" "Text superscript one\n\n" "Text subscript one\n\n" ) length = len(text) docEditor.replaceText(text) assert len(docEditor.getText()) == length # Show the ToolBar assert docToolBar.isVisible() is False docEditor._toggleToolBarVisibility() assert docToolBar.isVisible() is True # Markdown # ======== # Click Bold docEditor.setCursorPosition(20) docToolBar.tbBoldMD.click() assert len(docEditor.getText()) == length + 4 # Click Italic docEditor.setCursorPosition(54) docToolBar.tbItalicMD.click() assert len(docEditor.getText()) == length + 6 # Click Strikethrough docEditor.setCursorPosition(90) docToolBar.tbStrikeMD.click() assert len(docEditor.getText()) == length + 10 # Shortcodes # ========== # Click Bold docEditor.setCursorPosition(39) docToolBar.tbBold.click() assert len(docEditor.getText()) == length + 17 # Click Italic docEditor.setCursorPosition(80) docToolBar.tbItalic.click() assert len(docEditor.getText()) == length + 24 # Click Strikethrough docEditor.setCursorPosition(132) docToolBar.tbStrike.click() assert len(docEditor.getText()) == length + 31 # Click Underline docEditor.setCursorPosition(163) docToolBar.tbUnderline.click() assert len(docEditor.getText()) == length + 38 # Click Superscript docEditor.setCursorPosition(190) docToolBar.tbSuperscript.click() assert len(docEditor.getText()) == length + 49 # Click Subscript docEditor.setCursorPosition(223) docToolBar.tbSubscript.click() assert len(docEditor.getText()) == length + 60 # Check Result assert docEditor.getText() == ( "### A Scene\n\n" "Text **bold** one\n\n" "Text [b]bold[/b] two\n\n" "Text _italic_ one\n\n" "Text [i]italic[/i] two\n\n" "Text ~~strikethrough~~ one\n\n" "Text [s]strikethrough[/s] two\n\n" "Text [u]underline[/u] one\n\n" "Text [sup]superscript[/sup] one\n\n" "Text [sub]subscript[/sub] one\n\n" ) # qtbot.stop() @pytest.mark.gui def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the document insert functions.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor text = "### A Scene\n\n%s" % ipsumText[0] # Insert Text # =========== # No Document Handle docEditor.replaceText(text) docEditor._docHandle = None docEditor.setCursorPosition(24) docEditor.insertText("Stuff") assert docEditor.getText() == text docEditor._docHandle = C.hSceneDoc # Insert String docEditor.replaceText(text) docEditor.setCursorPosition(24) docEditor.insertText(", ipsumer,") assert docEditor.getText() == text[:24] + ", ipsumer," + text[24:] # Single Quotes docEditor.replaceText(text) docEditor.setCursorPosition(41) docEditor.insertText(nwDocInsert.QUOTE_LS) docEditor.setCursorPosition(53) docEditor.insertText(nwDocInsert.QUOTE_RS) assert docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") # Double Quotes docEditor.replaceText(text) docEditor.setCursorPosition(41) docEditor.insertText(nwDocInsert.QUOTE_LD) docEditor.setCursorPosition(53) docEditor.insertText(nwDocInsert.QUOTE_RD) assert docEditor.getText() == text.replace("consectetur", "\u201cconsectetur\u201d") # Invalid Inserts docEditor.replaceText(text) docEditor.insertText(nwDocInsert.NO_INSERT) assert docEditor.getText() == text docEditor.insertText(123) assert docEditor.getText() == text # Insert Comments # =============== docEditor.replaceText(text) count = docEditor.document().characterCount() # Invalid Position docEditor.setCursorPosition(12) docEditor.insertText(nwDocInsert.FOOTNOTE) assert docEditor.getText() == text # Valid Position docEditor.setCursorPosition(count) docEditor.insertText(nwDocInsert.FOOTNOTE) assert "[footnote:" in docEditor.getText() assert "%Footnote." in docEditor.getText() assert docEditor.getCursorPosition() > count # Insert KeyWords # =============== text = "### A Scene\n\n\n%s" % ipsumText[0] docEditor.replaceText(text) docEditor.setCursorLine(3) # Invalid Keyword docEditor.insertKeyWord("stuff") assert docEditor.getText() == text # Invalid Block with monkeypatch.context() as mp: mp.setattr(QTextBlock, "isValid", lambda *a, **k: False) docEditor.insertKeyWord(nwKeyWords.POV_KEY) assert docEditor.getText() == text # Valid Keyword docEditor.insertKeyWord(nwKeyWords.POV_KEY) docEditor.insertText("Jane\n") assert docEditor.getText() == text.replace( "\n\n\n", "\n\n@pov: Jane\n\n", 1 ) # Insert In-Block docEditor.setCursorPosition(20) docEditor.insertKeyWord(nwKeyWords.CHAR_KEY) docEditor.insertText("John") assert docEditor.getText() == text.replace( "\n\n\n", "\n\n@pov: Jane\n@char: John\n\n", 1 ) # qtbot.stop() @pytest.mark.gui def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the text manipulation functions.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) docEditor.replaceText(text) # Wrap Selection # ============== text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) docEditor.replaceText(text) docEditor.setCursorPosition(45) # Wrap Equal docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._wrapSelection("=") assert docEditor.getText() == text.replace("consectetur", "=consectetur=") # Wrap Unequal docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._wrapSelection("=", "*") assert docEditor.getText() == text.replace("consectetur", "=consectetur*") # Past Paragraph docEditor.replaceText(text) cursor = docEditor.textCursor() cursor.setPosition(13, QTextCursor.MoveAnchor) cursor.setPosition(1000, QTextCursor.KeepAnchor) docEditor.setTextCursor(cursor) docEditor._wrapSelection("=") newText = docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) assert newPara[1] == "="+ipsumText[0]+"=" assert newPara[2] == ipsumText[1] # Toggle Format # ============= text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) # Block format repetition docEditor.replaceText(text) docEditor.setCursorPosition(39) docEditor._toggleFormat(1, "=") assert docEditor.getText() == text.replace("amet", "=amet=", 1) docEditor._toggleFormat(1, "=") assert docEditor.getText() == text.replace("amet", "=amet=", 1) # Wrap Single Equal docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._toggleFormat(1, "=") assert docEditor.getText() == text.replace("consectetur", "=consectetur=") # Past Paragraph docEditor.replaceText(text) cursor = docEditor.textCursor() cursor.setPosition(13, QTextCursor.MoveAnchor) cursor.setPosition(1000, QTextCursor.KeepAnchor) docEditor.setTextCursor(cursor) docEditor._toggleFormat(1, "=") newText = docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) assert newPara[1] == "="+ipsumText[0]+"=" assert newPara[2] == ipsumText[1] # Wrap Double Equal docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._toggleFormat(2, "=") assert docEditor.getText() == text.replace("consectetur", "==consectetur==") # Toggle Double Equal with Selection docEditor.replaceText(text) docEditor.setCursorSelection(41, 11) docEditor._toggleFormat(2, "=") assert docEditor.getText() == text.replace("consectetur", "==consectetur==") assert docEditor.getSelectedText() == "consectetur" docEditor._toggleFormat(2, "=") assert docEditor.getText() == text assert docEditor.getSelectedText() == "consectetur" # Toggle Double Equal docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._toggleFormat(2, "=") docEditor._toggleFormat(2, "=") assert docEditor.getText() == text # Toggle Triple+Double Equal docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._toggleFormat(3, "=") docEditor._toggleFormat(2, "=") assert docEditor.getText() == text.replace("consectetur", "=consectetur=") # Toggle Unequal repText = text.replace("consectetur", "=consectetur==") docEditor.replaceText(repText) docEditor.setCursorPosition(45) docEditor._toggleFormat(1, "=") assert docEditor.getText() == text.replace("consectetur", "consectetur=") docEditor._toggleFormat(1, "=") assert docEditor.getText() == repText # Replace Quotes # ============== # No Selection text = "### A Scene\n\n%s" % ipsumText[0].replace("consectetur", "=consectetur=") docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._replaceQuotes("=", "<", ">") assert docEditor.getText() == text # First Paragraph Selected # This should not replace anything in second paragraph text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]).replace("ipsum", "=ipsum=") docEditor.replaceText(text) docEditor.setCursorPosition(45) assert docEditor.docAction(nwDocAction.SEL_PARA) docEditor._replaceQuotes("=", "<", ">") newText = docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) assert newPara[1] == ipsumText[0].replace("ipsum", "") assert newPara[2] == ipsumText[1].replace("ipsum", "=ipsum=") # Edge of Document text = ipsumText[0].replace("Lorem", "=Lorem=") docEditor.replaceText(text) docEditor.setCursorPosition(45) assert docEditor.docAction(nwDocAction.SEL_ALL) docEditor._replaceQuotes("=", "<", ">") assert docEditor.getText() == text.replace("=Lorem=", "") # Remove Line Breaks # ================== parOne = ipsumText[0].replace(" ", "\n", 5) parTwo = ipsumText[1].replace(" ", "\n", 5) # Check Blocks cursor = docEditor.textCursor() cursor.clearSelection() text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) docEditor.replaceText(text) docEditor.setCursorPosition(45) assert len(docEditor._selectedBlocks(cursor)) == 0 cursor.select(QTextCursor.SelectionType.Document) assert len(docEditor._selectedBlocks(cursor)) == 15 # Remove All text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) docEditor.replaceText(text) docEditor.setCursorPosition(45) docEditor._removeInParLineBreaks() assert docEditor.getText() == "### A Scene\n\n%s\n" % "\n\n".join(ipsumText[0:2]) # Remove in First Paragraph # Second paragraphs should remain unchanged text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) docEditor.replaceText(text) cursor = docEditor.textCursor() cursor.setPosition(16, QTextCursor.MoveAnchor) cursor.setPosition(680, QTextCursor.KeepAnchor) docEditor.setTextCursor(cursor) docEditor._removeInParLineBreaks() newText = docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) twoBits = parTwo.split() assert newPara[1] == ipsumText[0] assert newPara[2] == twoBits[0] assert newPara[3] == twoBits[1] assert newPara[4] == twoBits[2] assert newPara[5] == twoBits[3] assert newPara[6] == twoBits[4] assert newPara[7] == " ".join(twoBits[5:]) # Key Press Events # ================ text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) docEditor.replaceText(text) assert docEditor.getText() == text # Select All qtbot.keyClick(docEditor, Qt.Key_A, modifier=Qt.ControlModifier, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Delete, delay=KEY_DELAY) assert docEditor.getText() == "" # Undo qtbot.keyClick(docEditor, Qt.Key_Z, modifier=Qt.ControlModifier, delay=KEY_DELAY) assert docEditor.getText() == text # Redo qtbot.keyClick(docEditor, Qt.Key_Y, modifier=Qt.ControlModifier, delay=KEY_DELAY) assert docEditor.getText() == "" # qtbot.stop() @pytest.mark.gui def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the block formatting function.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor # Invalid and Generic # =================== text = "### A Scene\n\n%s" % ipsumText[0] docEditor.replaceText(text) # Invalid Block docEditor.setCursorPosition(0) with monkeypatch.context() as mp: mp.setattr(QTextBlock, "isValid", lambda *a, **k: False) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is False # Keyword docEditor.replaceText("@pov: Jane\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is False assert docEditor.getText() == "@pov: Jane\n\n" assert docEditor.getCursorPosition() == 5 # Unsupported Format docEditor.replaceText("% Comment\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.NO_ACTION) is False # Block Stripping : Left Side # =========================== # Strip Comment w/Space docEditor.replaceText("% Comment\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Comment\n\n" assert docEditor.getCursorPosition() == 3 # Strip Comment wo/Space docEditor.replaceText("%Comment\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Comment\n\n" assert docEditor.getCursorPosition() == 4 # Strip Header 1 docEditor.replaceText("# Title\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 3 # Strip Header 2 docEditor.replaceText("## Title\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 2 # Strip Header 3 docEditor.replaceText("### Title\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 1 # Strip Header 4 docEditor.replaceText("#### Title\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 0 # Strip Novel Title docEditor.replaceText("#! Title\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 2 # Strip Unnumbered Chapter docEditor.replaceText("##! Title\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 1 # Strip Hard Scene docEditor.replaceText("###! Title\n\n") docEditor.setCursorPosition(6) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 1 # Strip Text docEditor.replaceText("Generic text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Generic text\n\n" assert docEditor.getCursorPosition() == 5 # Strip Left Angle Brackets : Double w/Space docEditor.replaceText(">> Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 2 # Strip Left Angle Brackets : Single w/Space docEditor.replaceText("> Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 3 # Strip Left Angle Brackets : Double wo/Space docEditor.replaceText(">>Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 3 # Strip Left Angle Brackets : Single wo/Space docEditor.replaceText(">Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 4 # Block Stripping : Right Side # ============================ # Strip Right Angle Brackets : Double w/Space docEditor.replaceText("Some text <<\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 5 # Strip Right Angle Brackets : Single w/Space docEditor.replaceText("Some text <\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 5 # Strip Right Angle Brackets : Double wo/Space docEditor.replaceText("Some text<<\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 5 # Strip Right Angle Brackets : Single wo/Space docEditor.replaceText("Some text<\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 5 # Block Stripping : Both Sides # ============================ docEditor.replaceText(">> Some text <<\n\n") assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" docEditor.replaceText(">Some text <<\n\n") assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" docEditor.replaceText(">Some text<\n\n") assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Some text\n\n" # New Formats # =========== # Comment docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert docEditor.getText() == "% Some text\n\n" assert docEditor.getCursorPosition() == 7 # Toggle Comment w/Space docEditor.replaceText("% Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 3 # Toggle Comment wo/Space docEditor.replaceText("%Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 4 # Toggle Ignore Text w/Space docEditor.replaceText("%~ Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_IGN) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 2 # Toggle Ignore Text wo/Space docEditor.replaceText("%~Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_IGN) is True assert docEditor.getText() == "Some text\n\n" assert docEditor.getCursorPosition() == 3 # Header 1 docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_H1) is True assert docEditor.getText() == "# Some text\n\n" assert docEditor.getCursorPosition() == 7 # Header 2 docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_H2) is True assert docEditor.getText() == "## Some text\n\n" assert docEditor.getCursorPosition() == 8 # Header 3 docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_H3) is True assert docEditor.getText() == "### Some text\n\n" assert docEditor.getCursorPosition() == 9 # Header 4 docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_H4) is True assert docEditor.getText() == "#### Some text\n\n" assert docEditor.getCursorPosition() == 10 # Novel Title docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_TTL) is True assert docEditor.getText() == "#! Some text\n\n" assert docEditor.getCursorPosition() == 8 # Unnumbered Chapter docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_UNN) is True assert docEditor.getText() == "##! Some text\n\n" assert docEditor.getCursorPosition() == 9 # Hard Scene docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.BLOCK_HSC) is True assert docEditor.getText() == "###! Some text\n\n" assert docEditor.getCursorPosition() == 10 # Left Indent docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.INDENT_L) is True assert docEditor.getText() == "> Some text\n\n" assert docEditor.getCursorPosition() == 7 # Right Indent docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.INDENT_R) is True assert docEditor.getText() == "Some text <\n\n" assert docEditor.getCursorPosition() == 5 # Right/Left Indent docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.INDENT_L) is True assert docEditor._formatBlock(nwDocAction.INDENT_R) is True assert docEditor.getText() == "> Some text <\n\n" assert docEditor.getCursorPosition() == 7 # Left Align docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.ALIGN_L) is True assert docEditor.getText() == "Some text <<\n\n" assert docEditor.getCursorPosition() == 5 # Right Align docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.ALIGN_R) is True assert docEditor.getText() == ">> Some text\n\n" assert docEditor.getCursorPosition() == 8 # Centre Align docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.ALIGN_C) is True assert docEditor.getText() == ">> Some text <<\n\n" assert docEditor.getCursorPosition() == 8 # Left/Right Align (Overrides) docEditor.replaceText("Some text\n\n") docEditor.setCursorPosition(5) assert docEditor._formatBlock(nwDocAction.ALIGN_L) is True assert docEditor._formatBlock(nwDocAction.ALIGN_R) is True assert docEditor.getText() == ">> Some text\n\n" assert docEditor.getCursorPosition() == 8 # Other Checks # ============ # Final Cursor Position Out of Range docEditor.replaceText("#### Title\n\n") docEditor.setCursorPosition(3) assert docEditor._formatBlock(nwDocAction.BLOCK_TXT) is True assert docEditor.getText() == "Title\n\n" assert docEditor.getCursorPosition() == 5 # Third Line # This also needs to add a new block docEditor.replaceText("#### Title\n\nThe Text\n\n") docEditor.setCursorLine(3) assert docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert docEditor.getText() == "#### Title\n\n% The Text\n\n" # qtbot.stop() @pytest.mark.gui def testGuiEditor_MultiBlockFormatting(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the block formatting function.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor text = "### A Scene\n\n@char: Jane, John\n\n" + "\n\n".join(ipsumText) + "\n\n" docEditor.replaceText(text) assert [x[:5] for x in docEditor.getText().splitlines()] == [ "### A", "", "@char", "", "Lorem", "", "Nulla", "", "Nulla", "", "Pelle", "", "Integ", "" ] # Toggle Comment cursor = docEditor.textCursor() cursor.setPosition(50) cursor.movePosition(QtMoveRight, QtKeepAnchor, 2000) docEditor.setTextCursor(cursor) docEditor._iterFormatBlocks(nwDocAction.BLOCK_COM) assert [x[:5] for x in docEditor.getText().splitlines()] == [ "### A", "", "@char", "", "% Lor", "", "% Nul", "", "% Nul", "", "% Pel", "", "Integ", "" ] # Un-toggle the second cursor = docEditor.textCursor() cursor.setPosition(800) docEditor.setTextCursor(cursor) docEditor._iterFormatBlocks(nwDocAction.BLOCK_COM) assert [x[:5] for x in docEditor.getText().splitlines()] == [ "### A", "", "@char", "", "% Lor", "", "Nulla", "", "% Nul", "", "% Pel", "", "Integ", "" ] # Un-toggle all cursor = docEditor.textCursor() cursor.setPosition(50) cursor.movePosition(QtMoveRight, QtKeepAnchor, 3000) docEditor.setTextCursor(cursor) docEditor._iterFormatBlocks(nwDocAction.BLOCK_COM) assert [x[:5] for x in docEditor.getText().splitlines()] == [ "### A", "", "@char", "", "Lorem", "", "Nulla", "", "Nulla", "", "Pelle", "", "Integ", "" ] # Toggle Ignore Text cursor = docEditor.textCursor() cursor.setPosition(50) cursor.movePosition(QtMoveRight, QtKeepAnchor, 2000) docEditor.setTextCursor(cursor) docEditor._iterFormatBlocks(nwDocAction.BLOCK_IGN) assert [x[:5] for x in docEditor.getText().splitlines()] == [ "### A", "", "@char", "", "%~ Lo", "", "%~ Nu", "", "%~ Nu", "", "%~ Pe", "", "Integ", "" ] # Clear all paragraphs cursor = docEditor.textCursor() cursor.setPosition(50) cursor.movePosition(QtMoveRight, QtKeepAnchor, 3000) docEditor.setTextCursor(cursor) docEditor._iterFormatBlocks(nwDocAction.BLOCK_TXT) assert [x[:5] for x in docEditor.getText().splitlines()] == [ "### A", "", "@char", "", "Lorem", "", "Nulla", "", "Nulla", "", "Pelle", "", "Integ", "" ] # Final text should be identical to initial text assert docEditor.getText() == text # qtbot.stop() @pytest.mark.gui def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the document editor tags functionality.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor # Create Scene text = "### A Scene\n\n@char: Jane, John\n\n@object: Gun\n\n@:\n\n" + ipsumText[0] + "\n\n" docEditor.replaceText(text) # Create Character text = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" cHandle = SHARED.project.newFile("Jane Doe", C.hCharRoot) assert nwGUI.openDocument(cHandle) is True docEditor.replaceText(text) nwGUI.saveDocument() assert nwGUI.projView.projTree.revealNewTreeItem(cHandle) docEditor.updateTagHighLighting() # Follow Tag # ========== assert nwGUI.openDocument(C.hSceneDoc) is True # Empty Block docEditor.setCursorLine(2) assert docEditor._processTag() is nwTrinary.NEUTRAL # Not On Tag docEditor.setCursorLine(1) assert docEditor._processTag() is nwTrinary.NEUTRAL # On Tag Keyword docEditor.setCursorPosition(15) assert docEditor._processTag() is nwTrinary.NEUTRAL # On Known Tag, No Follow docEditor.setCursorPosition(22) assert docEditor._processTag(follow=False) is nwTrinary.POSITIVE assert nwGUI.docViewer._docHandle is None # On Known Tag, Follow docEditor.setCursorPosition(22) position = docEditor.cursorRect().center() event = QMouseEvent( QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModCtrl ) assert nwGUI.docViewer._docHandle is None docEditor.mouseReleaseEvent(event) assert nwGUI.docViewer._docHandle == cHandle assert nwGUI.closeViewerPanel() is True assert nwGUI.docViewer._docHandle is None # On Unknown Tag, Create It assert "0000000000011" not in SHARED.project.tree docEditor.setCursorPosition(28) assert docEditor._processTag(create=True) is nwTrinary.NEGATIVE assert "0000000000011" in SHARED.project.tree # On Unknown Tag, Missing Root assert "0000000000012" not in SHARED.project.tree docEditor.setCursorPosition(42) assert docEditor._processTag(create=True) is nwTrinary.NEGATIVE oHandle = SHARED.project.tree.findRoot(nwItemClass.OBJECT) assert oHandle == "0000000000012" oItem = SHARED.project.tree["0000000000013"] assert oItem is not None assert oItem.itemParent == "0000000000012" docEditor.setCursorPosition(47) assert docEditor._processTag() is nwTrinary.NEUTRAL # qtbot.stop() @pytest.mark.gui def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd): """Test the document editor meta completer functionality.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True docEditor = nwGUI.docEditor # Create Character text = ( "# Jane Doe\n\n" "@tag: Jane\n\n" "# John Doe\n\n" "@tag: John\n\n" ) cHandle = SHARED.project.newFile("People", C.hCharRoot) assert nwGUI.openDocument(cHandle) is True docEditor.replaceText(text) nwGUI.saveDocument() assert nwGUI.projView.projTree.revealNewTreeItem(cHandle) docEditor.replaceText("") completer = docEditor._completer # Create Scene nwGUI.switchFocus(nwWidget.EDITOR) for c in "### Scene One": 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) # Type Keyword @ qtbot.keyClick(docEditor, "@", delay=KEY_DELAY) assert len(completer.actions()) == len(nwKeyWords.VALID_KEYS) # Type "c" to filer list to 2 qtbot.keyClick(docEditor, "c", delay=KEY_DELAY) assert len(completer.actions()) == 2 # Type "q" to filer list to 0 qtbot.keyClick(docEditor, "q", delay=KEY_DELAY) assert len(completer.actions()) == 0 # Delete character and go select @char qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY) assert len(completer.actions()) == 2 completer.actions()[0].trigger() assert docEditor.getText() == ( "### Scene One\n\n" "@char:" ) # The list of Characters should show up automatically qtbot.keyClick(docEditor, " ", delay=KEY_DELAY) assert [a.text() for a in completer.actions()] == ["Jane", "John"] # Typing "q" should clear the list qtbot.keyClick(docEditor, "q", delay=KEY_DELAY) assert [a.text() for a in completer.actions()] == [] # Deleting it and typing "a", should leave "Jane" qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY) qtbot.keyClick(docEditor, "a", delay=KEY_DELAY) assert [a.text() for a in completer.actions()] == ["Jane"] # Selecting "Jane" should insert it completer.actions()[0].trigger() qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY) assert docEditor.getText() == ( "### Scene One\n\n" "@char: Jane\n" ) # Start a new line with a nonsense keyword, which should be handled for c in "@: ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key_Backspace, delay=KEY_DELAY) # Send keypresses to the completer object qtbot.keyClick(docEditor, "@", delay=KEY_DELAY) assert len(completer.actions()) == len(nwKeyWords.VALID_KEYS) qtbot.keyClick(completer, "f", delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key_Down, delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(completer, " ", delay=KEY_DELAY) qtbot.keyClick(completer, "h", delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key_Down, delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key_Return, delay=KEY_DELAY) qtbot.keyClick(completer, Qt.Key_Escape, delay=KEY_DELAY) assert docEditor.getText() == ( "### Scene One\n\n" "@char: Jane\n" "@focus: John" ) # qtbot.stop() @pytest.mark.gui def testGuiEditor_CursorVisibility(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the custom ensure cursor visible feature.""" buildTestProject(nwGUI, projPath) nwGUI.openDocument(C.hSceneDoc) docEditor = nwGUI.docEditor docEditor.setPlainText( "### Scene\n\n" + "".join(["Text\n\n"]*100) ) assert docEditor.cursorIsVisible() is True docEditor.setCenterOnScroll(False) # Scroll Down cursor = docEditor.textCursor() cursor.setPosition(605) docEditor.setTextCursor(cursor) docEditor.verticalScrollBar().setValue(0) assert docEditor.verticalScrollBar().value() == 0 docEditor.ensureCursorVisibleNoCentre() assert docEditor.verticalScrollBar().value() > 0 assert docEditor.cursorIsVisible() is True # Scroll Up cursor = docEditor.textCursor() cursor.setPosition(0) docEditor.setTextCursor(cursor) docEditor.verticalScrollBar().setValue(200) assert docEditor.verticalScrollBar().value() > 100 docEditor.ensureCursorVisibleNoCentre() assert docEditor.verticalScrollBar().value() == 0 assert docEditor.cursorIsVisible() is True # qtbot.stop() @pytest.mark.gui def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the word counter.""" docEditor = nwGUI.docEditor class MockThreadPool: def __init__(self): self._objID = None def start(self, runObj, priority=0): self._objID = id(runObj) def objectID(self): return self._objID threadPool = MockThreadPool() monkeypatch.setattr(QThreadPool, "globalInstance", lambda *a: threadPool) docEditor.timerDoc.blockSignals(True) docEditor.timerSel.blockSignals(True) buildTestProject(nwGUI, projPath) # Run on an empty document docEditor._runDocumentTasks() assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" docEditor._updateDocCounts(0, 0, 0) assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" docEditor._runSelCounter() assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" docEditor._updateSelCounts(0, 0, 0) assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" # Open a document and populate it SHARED.project.tree[C.hSceneDoc]._initCount = 0 # type: ignore SHARED.project.tree[C.hSceneDoc]._wordCount = 0 # type: ignore assert nwGUI.openDocument(C.hSceneDoc) is True text = "\n\n".join(ipsumText) cC, wC, pC = standardCounter(text) docEditor.replaceText(text) # Check that a busy counter is blocked with monkeypatch.context() as mp: mp.setattr(docEditor.wCounterDoc, "isRunning", lambda *a: True) docEditor._runDocumentTasks() assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" with monkeypatch.context() as mp: mp.setattr(docEditor.wCounterSel, "isRunning", lambda *a: True) docEditor._runSelCounter() assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" # Run the full word counter docEditor._runDocumentTasks() assert threadPool.objectID() == id(docEditor.wCounterDoc) docEditor.wCounterDoc.run() # docEditor._updateDocCounts(cC, wC, pC) assert SHARED.project.tree[C.hSceneDoc]._charCount == cC # type: ignore assert SHARED.project.tree[C.hSceneDoc]._wordCount == wC # type: ignore assert SHARED.project.tree[C.hSceneDoc]._paraCount == pC # type: ignore assert docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})" # Select all text and run the selection word counter docEditor.docAction(nwDocAction.SEL_ALL) docEditor._runSelCounter() assert threadPool.objectID() == id(docEditor.wCounterSel) docEditor.wCounterSel.run() assert docEditor.docFooter.wordsText.text() == f"Words: {wC} selected" # qtbot.stop() @pytest.mark.gui def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): """Test the document editor search functionality.""" monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) assert nwGUI.openProject(prjLipsum) is True assert nwGUI.openDocument("4c4f28287af27") is True docEditor = nwGUI.docEditor docSearch = docEditor.docSearch origText = docEditor.getText() # Select the Word "est" docEditor.setCursorPosition(645) docEditor._makeSelection(QTextCursor.WordUnderCursor) cursor = docEditor.textCursor() assert cursor.selectedText() == "est" # Activate search nwGUI.mainMenu.aFind.activate(QAction.Trigger) assert docSearch.isVisible() assert docSearch.searchText == "est" # Find next by enter key monkeypatch.setattr(docSearch.searchBox, "hasFocus", lambda: True) qtbot.keyClick(docSearch.searchBox, Qt.Key_Return, delay=KEY_DELAY) assert abs(docEditor.getCursorPosition() - 1299) < 3 # Find next by button qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) assert abs(docEditor.getCursorPosition() - 1513) < 3 # Activate loop search docSearch.toggleLoop.activate(QAction.Trigger) assert docSearch.toggleLoop.isChecked() assert CONFIG.searchLoop is True # Find next by menu Search > Find Next nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 647) < 3 # Close search docSearch.cancelSearch.activate(QAction.Trigger) assert docSearch.isVisible() is False docEditor.setCursorPosition(15) # Toggle search again with header button qtbot.mouseClick(docEditor.docHeader.searchButton, QtMouseLeft, delay=KEY_DELAY) docSearch.setSearchText("") assert docSearch.isVisible() is True # Search for non-existing docEditor.setCursorPosition(0) docSearch.setSearchText("abcdef") qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) assert docEditor.getCursorPosition() < 3 # No result # Enable RegEx search docSearch.toggleRegEx.activate(QAction.Trigger) assert docSearch.toggleRegEx.isChecked() assert CONFIG.searchRegEx is True # Set invalid RegEx docEditor.setCursorPosition(0) docSearch.setSearchText(r"\bSus[") qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) assert docEditor.getCursorPosition() < 3 # No result # Set dangerous RegEx (issue #1015) # If this doesn't get caught, the app will hang docEditor.setCursorPosition(0) docSearch.setSearchText(r".*") qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) assert abs(docEditor.getCursorPosition() - 14) < 3 # Set valid RegEx docSearch.setSearchText(r"\bSus") qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) assert abs(docEditor.getCursorPosition() - 223) < 3 # Find next and then prev nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 324) < 3 nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 223) < 3 # Make RegEx case sensitive docSearch.toggleCase.activate(QAction.Trigger) assert docSearch.toggleCase.isChecked() assert CONFIG.searchCase is True # Find next/prev (one result) nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 626) < 3 nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 626) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 626) < 3 # Trigger replace nwGUI.mainMenu.aReplace.activate(QAction.Trigger) docSearch.setReplaceText("foo") # Disable RegEx case sensitive docSearch.toggleCase.activate(QAction.Trigger) assert docSearch.toggleCase.isChecked() is False assert CONFIG.searchCase is False # Toggle replace preserve case docSearch.toggleMatchCap.activate(QAction.Trigger) assert docSearch.toggleMatchCap.isChecked() assert CONFIG.searchMatchCap is True # Replace "Sus" with "Foo" via menu docEditor.setCursorPosition(605) nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) nwGUI.mainMenu.aReplaceNext.activate(QAction.Trigger) assert docEditor.getText()[623:634] == "Foopendisse" # Find next/prev to loop file nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 223) < 3 nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 1805) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 223) < 3 # Replace "sus" with "foo" via replace button qtbot.mouseClick(docSearch.replaceButton, QtMouseLeft, delay=KEY_DELAY) assert docEditor.getText()[220:228] == "foocipit" # Revert last two replaces assert docEditor.docAction(nwDocAction.UNDO) assert docEditor.docAction(nwDocAction.UNDO) assert docEditor.getText() == origText # Disable RegEx search docSearch.toggleRegEx.activate(QAction.Trigger) assert not docSearch.toggleRegEx.isChecked() assert CONFIG.searchRegEx is False # Close search and select "est" again docSearch.cancelSearch.activate(QAction.Trigger) docEditor.setCursorPosition(645) docEditor._makeSelection(QTextCursor.WordUnderCursor) cursor = docEditor.textCursor() assert cursor.selectedText() == "est" # Activate search again nwGUI.mainMenu.aFind.activate(QAction.Trigger) assert docSearch.isVisible() assert docSearch.searchText == "est" # Enable full word search docSearch.toggleWord.activate(QAction.Trigger) assert docSearch.toggleWord.isChecked() assert CONFIG.searchWord is True # Only one match nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 647) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 647) < 3 # Enable next doc search docSearch.toggleProject.activate(QAction.Trigger) assert docSearch.toggleProject.isChecked() assert CONFIG.searchNextFile is True # Next match nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert docEditor.docHandle == "2426c6f0ca922" # Next document nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 620) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(docEditor.getCursorPosition() - 1127) < 3 # Next doc, no match assert CONFIG.searchNextFile is True docSearch.setSearchText("abcdef") nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert docEditor.docHandle != "2426c6f0ca922" assert docEditor.docHandle == "04468803b92e1" nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert docEditor.docHandle != "04468803b92e1" assert docEditor.docHandle == "7a992350f3eb6" # Toggle Replace docEditor.beginReplace() # MonkeyPatch the focus cycle. We can't really test this very well, other than # check that the tabs aren't captured when the main editor has focus with monkeypatch.context() as mp: mp.setattr(docEditor, "hasFocus", lambda: True) mp.setattr(docSearch.searchBox, "hasFocus", lambda: False) mp.setattr(docSearch.replaceBox, "hasFocus", lambda: False) assert docEditor.focusNextPrevChild(True) is False assert docSearch.cycleFocus() is False with monkeypatch.context() as mp: mp.setattr(docEditor, "hasFocus", lambda: False) mp.setattr(docSearch.searchBox, "hasFocus", lambda: True) mp.setattr(docSearch.replaceBox, "hasFocus", lambda: False) assert docEditor.focusNextPrevChild(True) is True assert docSearch.cycleFocus() is True with monkeypatch.context() as mp: mp.setattr(docEditor, "hasFocus", lambda: False) mp.setattr(docSearch.searchBox, "hasFocus", lambda: False) mp.setattr(docSearch.replaceBox, "hasFocus", lambda: True) assert docEditor.focusNextPrevChild(True) is True assert docSearch.cycleFocus() is True docSearch.closeSearch() assert docSearch.isVisible() is False assert docEditor.focusNextPrevChild(True) is True # Replace Text # ============ docSearch.toggleCase.setChecked(True) docSearch.toggleWord.setChecked(False) docSearch.toggleRegEx.setChecked(False) docSearch.toggleLoop.setChecked(False) docSearch.toggleProject.setChecked(False) docEditor.setCursorPosition(0) # Replace Next docSearch.searchBox.setText("a") docSearch.replaceBox.setText("A") # No focus with monkeypatch.context() as mp: mp.setattr(docEditor, "anyFocus", lambda: False) docEditor.findNext() assert docEditor.textCursor().selectedText() == "" docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "" # Search not open docSearch.closeSearch() assert docSearch.isVisible() is False docEditor.findNext() assert docSearch.isVisible() is True docSearch.closeSearch() assert docSearch.isVisible() is False docEditor.replaceNext() assert docSearch.isVisible() is True docEditor.toggleSearch() assert docSearch.isVisible() is False docEditor.toggleSearch() assert docSearch.isVisible() is True # Find first entry docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "a" assert docEditor.getCursorPosition() == 64 # Treat the search as a user selection docEditor._lastFind = None docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "a" assert docEditor.getCursorPosition() == 92 # Iterate through the rest finds = [104, 123, 175, 197, 206, 211, 220, 238, 250, 250] for i in range(10): docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "a" assert docEditor.getCursorPosition() == finds[i] assert docEditor._lastFind == (249, 250) # Search for something that doesn't exist docSearch.searchBox.setText("x") docEditor._lastFind = None docEditor.replaceNext() assert docEditor.textCursor().selectedText() == "" # qtbot.stop() @pytest.mark.gui def testGuiEditor_StaticMethods(): """Test the document editor's static methods.""" # Check the method that decides if it is allowed to insert a space # before a colon using the French, Spanish, etc language feature assert GuiDocEditor._allowSpaceBeforeColon("", "") is True assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True