Files
Veronica Berglyd Olsen 42352e9500 Add some more type constants
2025-10-04 18:42:20 +02:00

2499 lines
87 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter Main GUI Editor Class Tester
==========================================
This file is a part of novelWriter
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
""" # noqa
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl
from PyQt6.QtGui import (
QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent,
QDropEvent, QFont, QInputMethodEvent, QMouseEvent, QTextBlock, QTextCursor,
QTextDocument, QTextOption
)
from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit
from novelwriter import CONFIG, SHARED
from novelwriter.common import decodeMimeHandles
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
from novelwriter.gui.doceditor import GuiDocEditor, TextAutoReplace, _TagAction
from novelwriter.gui.dochighlight import TextBlockData
from novelwriter.text.counting import standardCounter
from novelwriter.types import (
QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone,
QtMouseLeft, QtMoveAnchor, QtMoveRight, QtScrollAlwaysOff,
QtScrollAsNeeded, QtSelectDocument, QtSelectWord
)
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(QtSelectWord)
editor.setTextCursor(cursor)
editor._openContextFromCursor()
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(f"### Lorem Ipsum\n\n{ipsumText[0]}")
nwGUI.saveDocument()
# Check Defaults
qDoc = docEditor.document()
assert qDoc.defaultTextOption().alignment() == QtAlignLeft
assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded
assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded
assert docEditor._autoReplace._padChar == nwUnicode.U_NBSP
assert docEditor.docHeader.itemTitle.text() == (
"Novel \u203a New Folder \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.Flag.ShowTabsAndSpaces
assert qDoc.defaultTextOption().flags() & QTextOption.Flag.ShowLineAndParagraphSeparators
assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff
assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff
assert docEditor._autoReplace._padChar == 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{0}".format("\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{0}".format("\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_DragAndDrop(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test drag and drop in the editor."""
docEditor = nwGUI.docEditor
buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hTitlePage) is True
assert docEditor.docHandle == C.hTitlePage
middle = docEditor.viewport().rect().center()
action = Qt.DropAction.MoveAction
mouse = Qt.MouseButton.NoButton
model = SHARED.project.tree.model
docMime = model.mimeData([model.indexFromHandle(C.hSceneDoc)])
noneMime = QMimeData()
noneMime.setData("plain/text", b"")
assert decodeMimeHandles(docMime) == [C.hSceneDoc]
# Drag Enter
mockEnter = MagicMock()
docEvent = QDragEnterEvent(middle, action, docMime, mouse, QtModNone)
noneEvent = QDragEnterEvent(middle, action, noneMime, mouse, QtModNone)
with monkeypatch.context() as mp:
mp.setattr(QPlainTextEdit, "dragEnterEvent", mockEnter)
# Document Enter
docEditor.dragEnterEvent(docEvent)
assert docEvent.isAccepted() is True
assert mockEnter.call_count == 0
# Regular Enter
docEditor.dragEnterEvent(noneEvent)
assert mockEnter.call_count == 1
# Drag Move
mockMove = MagicMock()
docEvent = QDragMoveEvent(middle, action, docMime, mouse, QtModNone)
noneEvent = QDragMoveEvent(middle, action, noneMime, mouse, QtModNone)
with monkeypatch.context() as mp:
mp.setattr(QPlainTextEdit, "dragMoveEvent", mockMove)
# Document Move
docEditor.dragMoveEvent(docEvent)
assert docEvent.isAccepted() is True
assert mockMove.call_count == 0
# Regular Move
docEditor.dragMoveEvent(noneEvent)
assert mockMove.call_count == 1
# Drop
middle = QPointF(docEditor.viewport().rect().center())
mockDrop = MagicMock()
docEvent = QDropEvent(middle, action, docMime, mouse, QtModNone)
noneEvent = QDropEvent(middle, action, noneMime, mouse, QtModNone)
with monkeypatch.context() as mp:
mp.setattr(QPlainTextEdit, "dropEvent", mockDrop)
# Document Drop
docEditor.dropEvent(docEvent)
assert mockDrop.call_count == 0
assert docEditor.docHandle == C.hSceneDoc
# Regular Move
docEditor.dropEvent(noneEvent)
assert mockDrop.call_count == 1
@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)
monkeypatch.setattr(QMenu, "setParent", 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\n"
"Some text ...\n\n"
"... and a link to http://example.com\n\n"
)
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()
# Open Link
ctxMenu = getMenuForPos(docEditor, 63)
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open URL", "Paste",
"Select All", "Select Word", "Select Paragraph"
]
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
clipboard = QApplication.clipboard()
assert clipboard is not None
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"
]
clipboard.clear()
ctxMenu.actions()[1].trigger()
assert clipboard.text(QClipboard.Mode.Clipboard) == "text"
# Cut Text
clipboard.clear()
ctxMenu.actions()[0].trigger()
assert 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)
monkeypatch.setattr(QMenu, "setParent", lambda *a: None)
buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True
docEditor = nwGUI.docEditor
text = "### A Scene\n\n{0}".format("\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)
LORAX = "Lorax\U0001F03A"
cursor = docEditor.textCursor()
cursor.setPosition(16)
data = cursor.block().userData()
assert cursor.block().text().startswith("Lorem")
assert isinstance(data, TextBlockData)
data._spellErrors = [(0, 5, "Lorem")]
# No known position
assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, [])
# With Suggestion
with monkeypatch.context() as mp:
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [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()
# Update Entry
data._spellErrors = [(0, 7, LORAX)]
# Without Suggestion
with monkeypatch.context() as mp:
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [])
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(SHARED.spelling, "suggestWords", lambda *a: [])
ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert "Ignore Word" in actions
assert "Add Word to Dictionary" in actions
assert LORAX not in SHARED.spelling._userDict
ctxMenu.actions()[7].trigger() # Ignore
assert LORAX not in SHARED.spelling._userDict
ctxMenu.actions()[8].trigger() # Add
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{0}".format("\n\n".join(ipsumText))
docEditor.replaceText(text)
doc = docEditor.document()
# Select/Cut/Copy/Paste/Undo/Redo
# ===============================
clipboard = QApplication.clipboard()
assert clipboard is not None
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]
clipboard.clear()
# Emphasis/Undo/Redo
# ==================
text = f"### A Scene\n\n{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
# Mark
docEditor.setCursorPosition(50)
assert docEditor.docAction(nwDocAction.MD_MARK) 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 = f"### A Scene\n\n{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 = f"### A Scene\n\n{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 = f"### A Scene\n\n{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 = f"### A Scene\n\n{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 = f"### A Scene\n\n\n{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{0}".format("\n\n".join(ipsumText))
docEditor.replaceText(text)
# Wrap Selection
# ==============
text = "### A Scene\n\n{0}".format("\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, QtMoveAnchor)
cursor.setPosition(1000, QtKeepAnchor)
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{0}".format("\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, QtMoveAnchor)
cursor.setPosition(1000, QtKeepAnchor)
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{0}".format(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{0}".format("\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", "<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=", "<Lorem>")
# Remove Line Breaks
# ==================
parOne = ipsumText[0].replace(" ", "\n", 5)
parTwo = ipsumText[1].replace(" ", "\n", 5)
# Check Blocks
cursor = docEditor.textCursor()
cursor.clearSelection()
text = f"### A Scene\n\n{parOne}\n\n{parTwo}"
docEditor.replaceText(text)
docEditor.setCursorPosition(45)
assert len(docEditor._selectedBlocks(cursor)) == 0
cursor.select(QtSelectDocument)
assert len(docEditor._selectedBlocks(cursor)) == 15
# Remove All
text = f"### A Scene\n\n{parOne}\n\n{parTwo}"
docEditor.replaceText(text)
docEditor.setCursorPosition(45)
docEditor._removeInParLineBreaks()
assert docEditor.getText() == "### A Scene\n\n{0}\n".format("\n\n".join(ipsumText[0:2]))
# Remove in First Paragraph
# Second paragraphs should remain unchanged
text = f"### A Scene\n\n{parOne}\n\n{parTwo}"
docEditor.replaceText(text)
cursor = docEditor.textCursor()
cursor.setPosition(16, QtMoveAnchor)
cursor.setPosition(680, QtKeepAnchor)
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 = f"### A Scene\n\n{parOne}\n\n{parTwo}"
docEditor.replaceText(text)
assert docEditor.getText() == text
# Select All
qtbot.keyClick(docEditor, Qt.Key.Key_A, modifier=QtModCtrl, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Delete, delay=KEY_DELAY)
assert docEditor.getText() == ""
# Undo
qtbot.keyClick(docEditor, Qt.Key.Key_Z, modifier=QtModCtrl, delay=KEY_DELAY)
assert docEditor.getText() == text
# Redo
qtbot.keyClick(docEditor, Qt.Key.Key_Y, modifier=QtModCtrl, 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 = f"### A Scene\n\n{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()
# Follow Tag
# ==========
assert nwGUI.openDocument(C.hSceneDoc) is True
# Empty Block
docEditor.setCursorLine(2)
assert docEditor._processTag() == _TagAction.NONE
# Not On Tag
docEditor.setCursorLine(1)
assert docEditor._processTag() == _TagAction.NONE
# On Tag Keyword
docEditor.setCursorPosition(15)
assert docEditor._processTag() == _TagAction.NONE
# On Known Tag, No Follow
docEditor.setCursorPosition(22)
assert docEditor._processTag(follow=False) == _TagAction.FOLLOW
assert nwGUI.docViewer._docHandle is None
# qtbot.stop()
# On Known Tag, Follow
docEditor.setCursorPosition(22)
position = QPointF(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) == _TagAction.CREATE
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) == _TagAction.CREATE
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() == _TagAction.NONE
# qtbot.stop()
@pytest.mark.gui
def testGuiEditor_Links(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd):
"""Test the document editor links functionality."""
buildTestProject(nwGUI, projPath)
nwGUI.openDocument(C.hSceneDoc)
docEditor = nwGUI.docEditor
docEditor.replaceText("### Scene\n\nFoo http://www.example.com bar.\n\n")
docEditor.setCursorPosition(20)
position = QPointF(docEditor.cursorRect().center())
event = QMouseEvent(
QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModCtrl
)
with monkeypatch.context() as mp:
openUrl = MagicMock()
mp.setattr(QDesktopServices, "openUrl", openUrl)
docEditor.mouseReleaseEvent(event)
assert openUrl.called is True
assert openUrl.call_args[0][0] == QUrl("http://www.example.com")
# 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()
docEditor.replaceText("")
completer = docEditor._completer
# Create Scene
nwGUI.docEditor.setFocus()
for c in "### Scene One":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.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.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.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.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.Key_Backspace, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.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.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, " ", delay=KEY_DELAY)
qtbot.keyClick(completer, "h", delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Escape, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
)
# Send keypresses to the completer object for a comment
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
)
# Auto-complete story comment
SHARED.project.index._itemIndex._cache.story.add("Resolution")
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, ".", delay=KEY_DELAY)
assert len(completer.actions()) == 1
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
"%Story.Resolution: \n"
)
# Auto-complete note comment
SHARED.project.index._itemIndex._cache.note.add("Consistency")
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, ".", delay=KEY_DELAY)
assert len(completer.actions()) == 1
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
"%Story.Resolution: \n"
"%Note.Consistency: \n"
)
# CJK completer reposition (#2267 and #2517)
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert completer.isVisible() is True
completer.move(0, 0)
assert completer.pos().x() == 0 # Completer menu at 0
assert completer.pos().y() == 0 # Completer menu at 0
event = QInputMethodEvent()
event.setCommitString("Text")
docEditor.inputMethodEvent(event)
assert completer.pos().x() > 0 # Completer should have moved
assert completer.pos().y() > 0 # Completer should have moved
# 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]._wordInit = 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()
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"Selected: {wC}"
# 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(663)
docEditor._makeSelection(QtSelectWord)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
# Activate search
nwGUI.mainMenu.aFind.activate(QAction.ActionEvent.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.Key_Return, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 1317) < 3
# Find next by button
qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 1531) < 3
# Activate loop search
docSearch.toggleLoop.activate(QAction.ActionEvent.Trigger)
assert docSearch.toggleLoop.isChecked()
assert CONFIG.searchLoop is True
# Find next by menu Search > Find Next
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 665) < 3
# Close search
docSearch.cancelSearch.activate(QAction.ActionEvent.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.ActionEvent.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() - 241) < 3
# Find next and then prev
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 342) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 241) < 3
# Make RegEx case sensitive
docSearch.toggleCase.activate(QAction.ActionEvent.Trigger)
assert docSearch.toggleCase.isChecked()
assert CONFIG.searchCase is True
# Find next/prev (one result)
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 644) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 644) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 644) < 3
# Trigger replace
nwGUI.mainMenu.aReplace.activate(QAction.ActionEvent.Trigger)
docSearch.setReplaceText("foo")
# Disable RegEx case sensitive
docSearch.toggleCase.activate(QAction.ActionEvent.Trigger)
assert docSearch.toggleCase.isChecked() is False
assert CONFIG.searchCase is False
# Toggle replace preserve case
docSearch.toggleMatchCap.activate(QAction.ActionEvent.Trigger)
assert docSearch.toggleMatchCap.isChecked()
assert CONFIG.searchMatchCap is True
# Replace "Sus" with "Foo" via menu
docEditor.setCursorPosition(623)
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aReplaceNext.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[641:652] == "Foopendisse"
# Find next/prev to loop file
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 241) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 1823) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 241) < 3
# Replace "sus" with "foo" via replace button
qtbot.mouseClick(docSearch.replaceButton, QtMouseLeft, delay=KEY_DELAY)
assert docEditor.getText()[238:246] == "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.ActionEvent.Trigger)
assert not docSearch.toggleRegEx.isChecked()
assert CONFIG.searchRegEx is False
# Close search and select "est" again
docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger)
docEditor.setCursorPosition(663)
docEditor._makeSelection(QtSelectWord)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
# Activate search again
nwGUI.mainMenu.aFind.activate(QAction.ActionEvent.Trigger)
assert docSearch.isVisible()
assert docSearch.searchText == "est"
# Enable full word search
docSearch.toggleWord.activate(QAction.ActionEvent.Trigger)
assert docSearch.toggleWord.isChecked()
assert CONFIG.searchWord is True
# Only one match
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 665) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 665) < 3
# Enable next doc search
docSearch.toggleProject.activate(QAction.ActionEvent.Trigger)
assert docSearch.toggleProject.isChecked()
assert CONFIG.searchNextFile is True
# Next match
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert docEditor.docHandle == "2426c6f0ca922" # Next document
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 651) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 1157) < 3
# Next doc, no match
assert CONFIG.searchNextFile is True
docSearch.setSearchText("abcdef")
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert docEditor.docHandle != "2426c6f0ca922"
assert docEditor.docHandle == "04468803b92e1"
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.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() == 83
# Iterate through the rest
finds = [85, 105, 110, 141, 169, 181, 200, 252, 274, 283, 288, 297]
for i in range(len(finds)):
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == "a"
assert docEditor.getCursorPosition() == finds[i]
assert docEditor._lastFind == (296, 297)
# 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_TextAutoReplaceSymbols():
"""Test the editor auto-replace functionality."""
CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO
CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO
CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO
CONFIG.doReplaceSQuote = True
CONFIG.doReplaceDQuote = True
CONFIG.doReplaceDash = True
CONFIG.doReplaceDots = True
ar = TextAutoReplace()
def prep(text: str) -> tuple[str, int]:
return text, len(text)
# Double Quote Open
assert ar._determine(*prep('"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('Stuff "')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('>"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('>>"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('_"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep(' _"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('\u00a0_"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('**"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep(' **"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('\u00a0**"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('=="')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep(' =="')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('\u00a0=="')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('~~"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep(' ~~"')) == (1, nwUnicode.U_LDQUO)
assert ar._determine(*prep('\u00a0~~"')) == (1, nwUnicode.U_LDQUO)
# Double Quote Close
assert ar._determine(*prep('Stuff"')) == (1, nwUnicode.U_RDQUO)
# Single Quote Open
assert ar._determine(*prep("'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("Stuff '")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep(">'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep(">>'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("_'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep(" _'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("\u00a0_'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("**'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep(" **'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("\u00a0**'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("=='")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep(" =='")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("\u00a0=='")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("~~'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep(" ~~'")) == (1, nwUnicode.U_LSQUO)
assert ar._determine(*prep("\u00a0~~'")) == (1, nwUnicode.U_LSQUO)
# Single Quote Close
assert ar._determine(*prep("Stuff'")) == (1, nwUnicode.U_RSQUO)
# Dashes
assert ar._determine(*prep("-")) == (0, "-")
assert ar._determine(*prep("--")) == (2, nwUnicode.U_ENDASH)
assert ar._determine(*prep("---")) == (3, nwUnicode.U_EMDASH)
assert ar._determine(*prep("----")) == (4, nwUnicode.U_HBAR)
assert ar._determine(*prep("\u2013-")) == (2, nwUnicode.U_EMDASH)
assert ar._determine(*prep("\u2014-")) == (2, nwUnicode.U_HBAR)
# Ellipsis
assert ar._determine(*prep(".")) == (0, ".")
assert ar._determine(*prep("..")) == (0, ".")
assert ar._determine(*prep("...")) == (3, nwUnicode.U_HELLIP)
# Block Typed Line Separator (#1150)
assert ar._determine(*prep("Text\u2028")) == (1, nwUnicode.U_PSEP)
@pytest.mark.gui
def testGuiEditor_TextAutoReplaceProcess():
"""Test the editor auto-replace functionality."""
CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO
CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO
CONFIG.doReplaceDQuote = True
CONFIG.doReplaceDots = True
ar = TextAutoReplace()
doc = QTextDocument()
def prep(text: str) -> tuple[str, QTextCursor]:
doc.setPlainText(text)
cursor = QTextCursor(doc)
cursor.setPosition(len(text))
return text, cursor
# Nothing to Process
assert ar.process(*prep("")) is False
# Standard Auto-Replace
assert ar.process(*prep("Text ...")) is True
assert doc.toRawText() == "Text \u2026"
# Pad Before, Normal
CONFIG.fmtPadBefore = ":\u00bb"
CONFIG.fmtPadThin = False
ar.initSettings()
assert ar.process(*prep("Text:")) is True
assert doc.toRawText() == "Text\u00a0:"
assert ar.process(*prep("Text :")) is True # See #1061
assert doc.toRawText() == "Text\u00a0:"
assert ar.process(*prep('Text"')) is True
assert doc.toRawText() == "Text\u00a0»"
assert ar.process(*prep("@Synopsis:")) is False
assert doc.toRawText() == "@Synopsis:"
# Pad Before, Thin
CONFIG.fmtPadBefore = ":\u00bb"
CONFIG.fmtPadThin = True
ar.initSettings()
assert ar.process(*prep("Text:")) is True
assert doc.toRawText() == "Text\u202f:"
assert ar.process(*prep("Text :")) is True # See #1061
assert doc.toRawText() == "Text\u202f:"
# Pad After, Normal
CONFIG.fmtPadAfter = "\u00ab"
CONFIG.fmtPadThin = False
ar.initSettings()
assert ar.process(*prep('Text "')) is True
assert doc.toRawText() == "Text «\u00a0"
# Pad After, Thin
CONFIG.fmtPadAfter = "\u00ab"
CONFIG.fmtPadThin = True
ar.initSettings()
assert ar.process(*prep('Text "')) is True
assert doc.toRawText() == "Text «\u202f"