From 191895507bf519d98ffdfa88be87b07763d4fd37 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 21 Sep 2020 20:17:07 +0200 Subject: [PATCH 01/51] Improved coverage of Edit, Insert and Format menus and document editor --- nw/gui/doceditor.py | 16 +- tests/reference/gui/1_0e17daca5f3e1.nwd | 6 +- tests/reference/gui/1_nwProject.nwx | 16 +- tests/test_gui.py | 229 ++++++++++++++++++++---- 4 files changed, 218 insertions(+), 49 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index a8e9de97..5a39a8cb 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -608,10 +608,8 @@ class GuiDocEditor(QTextEdit): """ if isinstance(theInsert, str): theText = theInsert - elif theInsert in nwDocInsert: - if theInsert == nwDocInsert.NO_INSERT: - theText = "" - elif theInsert == nwDocInsert.HARD_BREAK: + elif isinstance(theInsert, nwDocInsert): + if theInsert == nwDocInsert.HARD_BREAK: theText = " \n" elif theInsert == nwDocInsert.NB_SPACE: theText = nwUnicode.U_NBSP @@ -1177,17 +1175,17 @@ class GuiDocEditor(QTextEdit): theBlock = theCursor.block() if not theBlock.isValid(): logger.debug("Invalid block selected for action %s" % str(docAction)) - return + return False theText = theBlock.text() if len(theText.strip()) == 0: logger.debug("Empty block selected for action %s" % str(docAction)) - return + return False # Remove existing format first, if any if theText.startswith("@"): logger.error("Cannot apply block format to keyword/value line") - return + return False elif theText.startswith("% "): newText = theText[2:] cOffset = 2 @@ -1233,7 +1231,7 @@ class GuiDocEditor(QTextEdit): logger.error( "Unknown or unsupported block format requested: %s" % str(docAction) ) - return + return False # Replace the block text theCursor.beginEditBlock() @@ -1250,7 +1248,7 @@ class GuiDocEditor(QTextEdit): theCursor.endEditBlock() self.setTextCursor(theCursor) - return + return True def _makeSelection(self, selMode): """Wrapper function to select text based on a selection mode. diff --git a/tests/reference/gui/1_0e17daca5f3e1.nwd b/tests/reference/gui/1_0e17daca5f3e1.nwd index f7723099..3475abd2 100644 --- a/tests/reference/gui/1_0e17daca5f3e1.nwd +++ b/tests/reference/gui/1_0e17daca5f3e1.nwd @@ -19,5 +19,9 @@ This is a paragraph of dummy text. -This is another paragraph of much longer dummy text. It is in fact very very dumb dummy text! We can also try replacing “quotes”, even single’s quotes are replaced. We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either … +This is another paragraph of much longer dummy text. It is in fact very very dumb dummy text! We can also try replacing “quotes”, even single ‘quotes’ are replaced. Isn’t that nice? We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too. + +“Full line double quoted text.” + +‘Full line single quoted text.’ diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 5ef10130..1174c04c 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,11 +1,11 @@ - + New Project 4 1 - 8 + 11 True @@ -14,8 +14,8 @@ True 0e17daca5f3e1 None - 90 - 63 + 113 + 86 27 @@ -84,10 +84,10 @@ New True SCENE - 331 - 59 - 2 - 465 + 464 + 82 + 4 + 602 Plot diff --git a/tests/test_gui.py b/tests/test_gui.py index b7d34039..55cc46e4 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -12,7 +12,7 @@ from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QTextCursor from PyQt5.QtWidgets import QAction, QTreeWidgetItem -from nw.constants import nwItemType, nwDocAction, nwUnicode, nwOutline +from nw.constants import nwItemType, nwUnicode, nwOutline, nwDocAction, nwDocInsert keyDelay = 2 stepDelay = 20 @@ -223,12 +223,31 @@ def testMainWindow(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): "It is in fact very very dumb dummy text! " ): qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) - for c in "We can also try replacing \"quotes\", even single's quotes are replaced. ": + for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + for c in "Isn't that nice? ": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) for c in "Ellipsis? Not a problem either ... ": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + for c in "How about three hyphens - -": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Left, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Backspace, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Right, delay=keyDelay) + for c in "- for long dash? It works too.": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + + for c in "\"Full line double quoted text.\"": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + + for c in "'Full line single quoted text.'": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) @@ -298,7 +317,7 @@ def testMainWindow(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # qtbot.stopForInteraction() @pytest.mark.gui -def testDocAction(qtbot, nwLipsum, nwTemp): +def testEditFormatMenu(qtbot, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -306,6 +325,9 @@ def testDocAction(qtbot, nwLipsum, nwTemp): qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) + # Test Document Action with No Project + assert not nwGUI.docEditor.docAction(nwDocAction.COPY) + nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) qtbot.wait(stepDelay) @@ -317,99 +339,232 @@ def testDocAction(qtbot, nwLipsum, nwTemp): cleanText = nwGUI.docEditor.getText()[27:74] # Bold - assert nwGUI.passDocumentAction(nwDocAction.STRONG) + nwGUI.mainMenu.aFmtStrong.activate(QAction.Trigger) fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:78] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.STRONG) + nwGUI.mainMenu.aFmtStrong.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Italic - assert nwGUI.passDocumentAction(nwDocAction.EMPH) + nwGUI.mainMenu.aFmtEmph.activate(QAction.Trigger) fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:76] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.EMPH) + nwGUI.mainMenu.aFmtEmph.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Strikethrough - assert nwGUI.passDocumentAction(nwDocAction.STRIKE) + nwGUI.mainMenu.aFmtStrike.activate(QAction.Trigger) fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:78] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.STRIKE) + nwGUI.mainMenu.aFmtStrike.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Should get us back to plain - assert nwGUI.passDocumentAction(nwDocAction.STRONG) + nwGUI.mainMenu.aFmtStrong.activate(QAction.Trigger) qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.EMPH) + nwGUI.mainMenu.aFmtEmph.activate(QAction.Trigger) qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.EMPH) + nwGUI.mainMenu.aFmtEmph.activate(QAction.Trigger) qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.STRONG) + nwGUI.mainMenu.aFmtStrong.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Double Quotes - assert nwGUI.passDocumentAction(nwDocAction.D_QUOTE) + nwGUI.mainMenu.aFmtDQuote.activate(QAction.Trigger) fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:76] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.UNDO) + nwGUI.mainMenu.aEditUndo.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Single Quotes - assert nwGUI.passDocumentAction(nwDocAction.S_QUOTE) + nwGUI.mainMenu.aFmtSQuote.activate(QAction.Trigger) fmtStr = "‘Pellentesque’ nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:76] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.UNDO) + nwGUI.mainMenu.aEditUndo.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Block Formats assert nwGUI.docEditor.setCursorPosition(30) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H1) + nwGUI.mainMenu.aFmtHead1.activate(QAction.Trigger) fmtStr = "# Pellentesque nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:76] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H2) + nwGUI.mainMenu.aFmtHead2.activate(QAction.Trigger) fmtStr = "## Pellentesque nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:77] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H3) + nwGUI.mainMenu.aFmtHead3.activate(QAction.Trigger) fmtStr = "### Pellentesque nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:78] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H4) + nwGUI.mainMenu.aFmtHead4.activate(QAction.Trigger) fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:79] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT) + nwGUI.mainMenu.aFmtNoFormat.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_COM) + nwGUI.mainMenu.aFmtComment.activate(QAction.Trigger) fmtStr = "% Pellentesque nec erat ut nulla posuere commodo." assert nwGUI.docEditor.getText()[27:76] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT) + nwGUI.mainMenu.aFmtNoFormat.activate(QAction.Trigger) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Check comment with no space before text + assert nwGUI.docEditor.setCursorPosition(27) + assert nwGUI.docEditor.insertText("%") + fmtStr = "%Pellentesque nec erat ut nulla posuere commodo." + assert nwGUI.docEditor.getText()[27:75] == fmtStr + qtbot.wait(stepDelay) + + nwGUI.mainMenu.aFmtNoFormat.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) # Undo/Redo - assert nwGUI.passDocumentAction(nwDocAction.UNDO) - fmtStr = "% Pellentesque nec erat ut nulla posuere commodo." - assert nwGUI.docEditor.getText()[27:76] == fmtStr + nwGUI.mainMenu.aEditUndo.activate(QAction.Trigger) + fmtStr = "%Pellentesque nec erat ut nulla posuere commodo." + assert nwGUI.docEditor.getText()[27:75] == fmtStr qtbot.wait(stepDelay) - assert nwGUI.passDocumentAction(nwDocAction.REDO) + nwGUI.mainMenu.aEditRedo.activate(QAction.Trigger) assert nwGUI.docEditor.getText()[27:74] == cleanText qtbot.wait(stepDelay) + # Cut, Copy and Paste + assert nwGUI.docEditor.setCursorPosition(27) + nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor) + + nwGUI.mainMenu.aEditCut.activate(QAction.Trigger) + assert nwGUI.docEditor.getText()[27:77] == ( + " nec erat ut nulla posuere commodo. Curabitur nisi" + ) + + nwGUI.mainMenu.aEditPaste.activate(QAction.Trigger) + assert nwGUI.docEditor.getText()[27:77] == ( + "Pellentesque nec erat ut nulla posuere commodo. Cu" + ) + + assert nwGUI.docEditor.setCursorPosition(27) + nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor) + + nwGUI.mainMenu.aEditCopy.activate(QAction.Trigger) + assert nwGUI.docEditor.getText()[27:77] == ( + "Pellentesque nec erat ut nulla posuere commodo. Cu" + ) + + assert nwGUI.docEditor.setCursorPosition(27) + nwGUI.mainMenu.aEditPaste.activate(QAction.Trigger) + assert nwGUI.docEditor.getText()[27:77] == ( + "PellentesquePellentesque nec erat ut nulla posuere" + ) + nwGUI.mainMenu.aEditUndo.activate(QAction.Trigger) + + # Select Paragraph/All + assert nwGUI.docEditor.setCursorPosition(30) + nwGUI.mainMenu.aSelectPar.activate(QAction.Trigger) + theCursor = nwGUI.docEditor.textCursor() + assert theCursor.selectedText() == ( + "Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta " + "imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit " + "placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. " + "Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. " + "Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, " + "rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin " + "nunc lacus, imperdiet nec posuere ac, interdum non lectus." + ) + + assert nwGUI.docEditor.setCursorPosition(30) + nwGUI.mainMenu.aSelectAll.activate(QAction.Trigger) + theCursor = nwGUI.docEditor.textCursor() + assert len(theCursor.selectedText()) == 1883 + + # Clear the Text + nwGUI.docEditor.clear() + assert nwGUI.docEditor.isEmpty() + + # Replace Quotes + nwGUI.docEditor.setText(( + "### New Text\n\n" + "Text with 'single' quotes and 'tricky stuff's'.\n\n" + "Also text with \"double\" quotes which are \"less tricky\".\n\n" + )) + + nwGUI.mainMenu.aSelectAll.activate(QAction.Trigger) + nwGUI.mainMenu.aFmtReplSng.activate(QAction.Trigger) + assert nwGUI.docEditor.getText() == ( + "### New Text\n\n" + "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n" + "Also text with \"double\" quotes which are \"less tricky\".\n\n" + ) + + nwGUI.mainMenu.aSelectAll.activate(QAction.Trigger) + nwGUI.mainMenu.aFmtReplDbl.activate(QAction.Trigger) + assert nwGUI.docEditor.getText() == ( + "### New Text\n\n" + "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n" + "Also text with “double” quotes which are “less tricky”.\n\n" + ) + + # Test Invalid Document Action + assert not nwGUI.docEditor.docAction(nwDocAction.NO_ACTION) + + # Test Invalid Formats + nwGUI.docEditor.setText(( + "### New Text\n\n" + "@tag: Bod\n\n" + "Text with 'single' quotes and 'tricky stuff's'.\n\n" + "Also text with \"double\" quotes which are \"less tricky\".\n\n" + )) + + # Cannot Format Tag + assert nwGUI.docEditor.setCursorPosition(17) + assert not nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_TXT) + + # Cannot Format Empty Line + assert nwGUI.docEditor.setCursorPosition(13) + assert not nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_TXT) + + # Invalid Action + assert nwGUI.docEditor.setCursorPosition(30) + assert not nwGUI.docEditor._formatBlock(nwDocAction.NO_ACTION) + + # Ensure No Changes + assert nwGUI.docEditor.getText() == ( + "### New Text\n\n" + "@tag: Bod\n\n" + "Text with 'single' quotes and 'tricky stuff's'.\n\n" + "Also text with \"double\" quotes which are \"less tricky\".\n\n" + ) + + # qtbot.stopForInteraction() + nwGUI.closeMain() + +def testContextMenu(qtbot, nwLipsum, nwTemp): + + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwGUI.theProject.projTree.setSeed(42) + assert nwGUI.openProject(nwLipsum) + assert nwGUI.openDocument("4c4f28287af27") + qtbot.wait(stepDelay) + # Editor Context Menu theCursor = nwGUI.docEditor.textCursor() theCursor.setPosition(100) @@ -492,16 +647,28 @@ def testInsertMenu(qtbot, nwFuncTemp, nwTemp): nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": nwFuncTemp}, True) - assert nwGUI.treeView._getTreeItem("31489056e0916") is not None + assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None nwGUI.setFocus(1) nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("31489056e0916").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView._getTreeItem("0e17daca5f3e1").setSelected(True) assert nwGUI.openSelectedItem() + nwGUI.docEditor.clear() + + # Test Faulty Inserts + assert nwGUI.docEditor.insertText("hello world") + assert nwGUI.docEditor.getText() == "hello world" + nwGUI.docEditor.clear() + + assert not nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) + assert nwGUI.docEditor.isEmpty() + + assert not nwGUI.docEditor.insertText(None) + assert nwGUI.docEditor.isEmpty() # qtbot.stopForInteraction() + # Check Menu Entries nwGUI.mainMenu.aInsENDash.activate(QAction.Trigger) assert nwGUI.docEditor.getText() == nwUnicode.U_ENDASH nwGUI.docEditor.clear() From de3b35fa6df5e0417052afeb2f298345928be2a8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 21 Sep 2020 22:07:21 +0200 Subject: [PATCH 02/51] Add Search/Replace test --- nw/gui/doceditor.py | 6 +- tests/test_gui.py | 131 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 5a39a8cb..ed36cfdf 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -423,10 +423,10 @@ class GuiDocEditor(QTextEdit): return True def getCursorPosition(self): - """Find the cursor position in the document. + """Find the cursor position in the document. If the editor has a + selection, return the position of the end of the selection. """ - theCursor = self.textCursor() - return theCursor.position() + return self.textCursor().selectionEnd() def setCursorLine(self, theLine): """Move the cursor to a given line in the document. diff --git a/tests/test_gui.py b/tests/test_gui.py index 55cc46e4..dca5da31 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -552,6 +552,7 @@ def testEditFormatMenu(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() +@pytest.mark.gui def testContextMenu(qtbot, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) @@ -726,6 +727,136 @@ def testInsertMenu(qtbot, nwFuncTemp, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() +@pytest.mark.gui +def testTextSearch(qtbot, nwLipsum, nwTemp): + + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwGUI.theProject.projTree.setSeed(42) + assert nwGUI.openProject(nwLipsum) + assert nwGUI.openDocument("4c4f28287af27") + origText = nwGUI.docEditor.getText() + qtbot.wait(stepDelay) + + # Select the Word "est" + assert nwGUI.docEditor.setCursorPosition(618) + nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor) + theCursor = nwGUI.docEditor.textCursor() + assert theCursor.selectedText() == "est" + + # Activate Search + nwGUI.mainMenu.aFind.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.isVisible() + assert nwGUI.docEditor.docSearch.getSearchText() == "est" + + # Find Next by Menu + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 1272) < 3 + + # Find Next by Button + qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=keyDelay) + assert abs(nwGUI.docEditor.getCursorPosition() - 1486) < 3 + + # Activate Loop Search + nwGUI.docEditor.docSearch.toggleLoop.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.toggleLoop.isChecked() + assert nwGUI.docEditor.docSearch.doLoop + + # Find Next by Menu Search > Find Next + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 + + # Close Search + nwGUI.docEditor.docSearch.cancelSearch.activate(QAction.Trigger) + assert not nwGUI.docEditor.docSearch.isVisible() + assert nwGUI.docEditor.setCursorPosition(15) + + # Toggle Search Again with Header Button + qtbot.mouseClick(nwGUI.docEditor.docHeader.searchButton, Qt.LeftButton, delay=keyDelay) + assert nwGUI.docEditor.docSearch.setSearchText("") + assert nwGUI.docEditor.docSearch.isVisible() + + # Enable RegEx Search + nwGUI.docEditor.docSearch.toggleRegEx.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.toggleRegEx.isChecked() + assert nwGUI.docEditor.docSearch.isRegEx + + # Set Invalid RegEx + assert nwGUI.docEditor.docSearch.setSearchText(r"\bSus[") + qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=keyDelay) + assert nwGUI.docEditor.getCursorPosition() < 3 # No result + + # Set Valid RegEx + assert nwGUI.docEditor.docSearch.setSearchText(r"\bSus") + qtbot.mouseClick(nwGUI.docEditor.docSearch.searchButton, Qt.LeftButton, delay=keyDelay) + assert abs(nwGUI.docEditor.getCursorPosition() - 196) < 3 + + # Find Next and then Prev + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 297) < 3 + nwGUI.mainMenu.aFindPrev.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 196) < 3 + + # Make RegEx Case Sensitive + nwGUI.docEditor.docSearch.toggleCase.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.toggleCase.isChecked() + assert nwGUI.docEditor.docSearch.isCaseSense + + # Find Next (One Result) + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 599) < 3 + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 599) < 3 + + # Trigger Replace + nwGUI.mainMenu.aReplace.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.setReplaceText("foo") + + # Disable RegEx Case Sensitive + nwGUI.docEditor.docSearch.toggleCase.activate(QAction.Trigger) + assert not nwGUI.docEditor.docSearch.toggleCase.isChecked() + assert not nwGUI.docEditor.docSearch.isCaseSense + + # Toggle Replace Preserve Case + nwGUI.docEditor.docSearch.toggleMatchCap.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.toggleMatchCap.isChecked() + assert nwGUI.docEditor.docSearch.doMatchCap + + # Replace "Sus" with "Foo" via Menu + nwGUI.mainMenu.aReplaceNext.activate(QAction.Trigger) + assert nwGUI.docEditor.getText()[596:607] == "Foopendisse" + + # Find Next to Loop File + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + + # Replace "sus" with "foo" via Replace Button + qtbot.mouseClick(nwGUI.docEditor.docSearch.replaceButton, Qt.LeftButton, delay=keyDelay) + assert nwGUI.docEditor.getText()[193:201] == "foocipit" + + # Revert Last Two Replaces + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText() == origText + + # Disable RegEx Search + nwGUI.docEditor.docSearch.toggleRegEx.activate(QAction.Trigger) + assert not nwGUI.docEditor.docSearch.toggleRegEx.isChecked() + assert not nwGUI.docEditor.docSearch.isRegEx + + # assert nwGUI.docEditor.setCursorPosition(0) + nwGUI.docEditor.docSearch.searchBox.setFocus(True) + assert nwGUI.docEditor.docSearch.cycleFocus(True) + assert nwGUI.docEditor.docSearch.replaceBox.hasFocus() + assert nwGUI.docEditor.docSearch.cycleFocus(True) + assert nwGUI.docEditor.docSearch.searchBox.hasFocus() + + # qtbot.stopForInteraction() + nwGUI.closeMain() + @pytest.mark.gui def testOutline(qtbot, nwLipsum, nwTemp): From 24e336cd771e6ad4862d32779703160d70926c34 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 21 Sep 2020 22:10:22 +0200 Subject: [PATCH 03/51] Try to fix xvfb display --- .github/workflows/pytest_cov.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest_cov.yml b/.github/workflows/pytest_cov.yml index 1969d200..850152dd 100644 --- a/.github/workflows/pytest_cov.yml +++ b/.github/workflows/pytest_cov.yml @@ -32,7 +32,10 @@ jobs: pip install pytest-qt pip install codecov - name: Run Tests - run: xvfb-run pytest -v --cov=nw --timeout=60 + run: | + export DISPLAY=':99.0' + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + xvfb-run pytest -v --cov=nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 From 7a9124ca5848425027684c34a1c47ea46130fdfe Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 21 Sep 2020 22:25:22 +0200 Subject: [PATCH 04/51] Finished Search/Replace test --- .github/workflows/pytest_cov.yml | 5 +--- tests/test_gui.py | 41 +++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pytest_cov.yml b/.github/workflows/pytest_cov.yml index 850152dd..1969d200 100644 --- a/.github/workflows/pytest_cov.yml +++ b/.github/workflows/pytest_cov.yml @@ -32,10 +32,7 @@ jobs: pip install pytest-qt pip install codecov - name: Run Tests - run: | - export DISPLAY=':99.0' - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - xvfb-run pytest -v --cov=nw --timeout=60 + run: xvfb-run pytest -v --cov=nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/tests/test_gui.py b/tests/test_gui.py index dca5da31..f437bc76 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -847,12 +847,41 @@ def testTextSearch(qtbot, nwLipsum, nwTemp): assert not nwGUI.docEditor.docSearch.toggleRegEx.isChecked() assert not nwGUI.docEditor.docSearch.isRegEx - # assert nwGUI.docEditor.setCursorPosition(0) - nwGUI.docEditor.docSearch.searchBox.setFocus(True) - assert nwGUI.docEditor.docSearch.cycleFocus(True) - assert nwGUI.docEditor.docSearch.replaceBox.hasFocus() - assert nwGUI.docEditor.docSearch.cycleFocus(True) - assert nwGUI.docEditor.docSearch.searchBox.hasFocus() + # Close Search and Select "est" Again + nwGUI.docEditor.docSearch.cancelSearch.activate(QAction.Trigger) + assert nwGUI.docEditor.setCursorPosition(618) + nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor) + theCursor = nwGUI.docEditor.textCursor() + assert theCursor.selectedText() == "est" + + # Activate Search Again + nwGUI.mainMenu.aFind.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.isVisible() + assert nwGUI.docEditor.docSearch.getSearchText() == "est" + + # Enable Full Word Search + nwGUI.docEditor.docSearch.toggleWord.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.toggleWord.isChecked() + assert nwGUI.docEditor.docSearch.isWholeWord + + # Only One Match + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 + + # Enable Next Doc Search + nwGUI.docEditor.docSearch.toggleProject.activate(QAction.Trigger) + assert nwGUI.docEditor.docSearch.toggleProject.isChecked() + assert nwGUI.docEditor.docSearch.doNextFile + + # Next Match + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert nwGUI.docEditor.theHandle == "2426c6f0ca922" # Next document + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 + nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + assert abs(nwGUI.docEditor.getCursorPosition() - 1127) < 3 # qtbot.stopForInteraction() nwGUI.closeMain() From 9a9cfe1ce58d41de8d1cc7dc615c35545f40f411 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 22 Sep 2020 21:37:00 +0200 Subject: [PATCH 05/51] Rename showGUI variable to blockGUI and added some comments in Config class --- nw/__init__.py | 2 +- nw/config.py | 40 +++++++++++++++++++--------------------- nw/core/project.py | 4 ++-- nw/error.py | 2 +- nw/gui/build.py | 4 ++-- nw/gui/docsplit.py | 2 +- nw/gui/preferences.py | 2 +- nw/gui/projload.py | 4 ++-- nw/gui/projtree.py | 4 ++-- nw/gui/writingstats.py | 4 ++-- nw/guimain.py | 30 +++++++++++++++--------------- tests/test_dialogs.py | 2 +- 12 files changed, 49 insertions(+), 51 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 715b1e2e..b43c064a 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -195,7 +195,7 @@ def main(sysArgs=None): testMode = True # Set Config Options - CONFIG.showGUI = not testMode + CONFIG.blockGUI = not testMode CONFIG.debugInfo = debugLevel < logging.INFO CONFIG.cmdOpen = cmdOpen diff --git a/nw/config.py b/nw/config.py index 5138a677..9af86619 100644 --- a/nw/config.py +++ b/nw/config.py @@ -57,32 +57,31 @@ class Config: self.cmdOpen = None # Debug Settings - self.showGUI = True - self.debugInfo = False + self.blockGUI = True # Allow blocking the GUI (disabled for testing) + self.debugInfo = False # True if log level is DEBUG or VERBOSE # Config Error Handling - self.hasError = False - self.errData = [] + self.hasError = False # True if the config class encountered an error + self.errData = [] # List of error messages # Set Paths - self.confPath = None - self.confFile = None - self.dataPath = None - self.homePath = None - self.lastPath = None - self.appPath = None - self.appRoot = None - self.appIcon = None - self.assetPath = None - self.themeRoot = None - self.graphPath = None - self.dictPath = None - self.iconPath = None - self.helpPath = None + self.confPath = None # Folder where the config is saved + self.confFile = None # The config file name + self.dataPath = None # Folder where app data is stored + self.homePath = None # The user's home folder + self.lastPath = None # The last user-selected folder (browse dialogs) + self.appPath = None # The full path to the novelwriter package folder + self.appRoot = None # The full path to the novelwriter root folder + self.appIcon = None # The full path to the novelwriter icon file + self.assetPath = None # The full path to the nw/assets folder + self.themeRoot = None # The full path to the nw/assets/themes folder + self.dictPath = None # The full path to the nw/assets/dict folder + self.iconPath = None # The full path to the nw/assets/icons folder + self.helpPath = None # The full path to the novelwriter .qhc help file # Runtime Settings and Variables - self.confChanged = False - self.hasHelp = False + self.confChanged = False # True whenever the config has chenged, false after save + self.hasHelp = False # True if the Qt help files are present in the assets folder ## General self.guiTheme = "default" @@ -264,7 +263,6 @@ class Config: self.appRoot = path.join(self.appPath, path.pardir) self.assetPath = path.join(self.appPath, "assets") self.themeRoot = path.join(self.assetPath, "themes") - self.graphPath = path.join(self.assetPath, "graphics") self.dictPath = path.join(self.assetPath, "dict") self.iconPath = path.join(self.assetPath, "icons") self.appIcon = path.join(self.iconPath, "novelwriter.svg") diff --git a/nw/core/project.py b/nw/core/project.py index 5bead8f5..442e76c7 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -513,7 +513,7 @@ class NWProject(): # Check novelWriter Version # ========================= - if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: + if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.blockGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( "This project was saved by a newer version of novelWriter, version %s. " @@ -926,7 +926,7 @@ class NWProject(): return False if path.isdir(projPath): - if self.mainConf.showGUI and listdir(self.projPath): + if self.mainConf.blockGUI and listdir(self.projPath): self.theParent.makeAlert(( "New project folder is not empty. " "Each project requires a dedicated project folder." diff --git a/nw/error.py b/nw/error.py index e1283fbd..00abc818 100644 --- a/nw/error.py +++ b/nw/error.py @@ -160,7 +160,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False): errMsg = NWErrorMessage(nwGUI) errMsg.setMessage(exType, exValue, exTrace) - if nw.CONFIG.showGUI: + if nw.CONFIG.blockGUI: errMsg.exec_() try: diff --git a/nw/gui/build.py b/nw/gui/build.py index b61dfd0f..155c6596 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -633,7 +633,7 @@ class GuiBuildNovel(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog saveTo = QFileDialog.getSaveFileName( @@ -745,7 +745,7 @@ class GuiBuildNovel(QDialog): errMsg = "Unknown format" # Report to user - if self.mainConf.showGUI: + if self.mainConf.blockGUI: if wSuccess: self.theParent.makeAlert( "%s file successfully written to:
%s" % ( diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 376eaf3a..8c3c4897 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -160,7 +160,7 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return - if self.mainConf.showGUI: + if self.mainConf.blockGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Split Document", ( diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index e0490f1a..eca12735 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -103,7 +103,7 @@ class GuiPreferences(PagedDialog): validEntries &= retA needsRestart |= retB - if needsRestart and self.mainConf.showGUI: + if needsRestart and self.mainConf.blockGUI: msgBox = QMessageBox() msgBox.information( self, "Preferences", diff --git a/nw/gui/projload.py b/nw/gui/projload.py index e8aed721..c21fc84c 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -177,7 +177,7 @@ class GuiProjectLoad(QDialog): """Browse for a folder path. """ logger.verbose("GuiProjectLoad browse button clicked") - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog projFile, _ = QFileDialog.getOpenFileName( @@ -218,7 +218,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: doRemove = False - if self.mainConf.showGUI: + if self.mainConf.blockGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Remove Entry", diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 0a9c3905..db1aa626 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -410,7 +410,7 @@ class GuiProjectTree(QTreeWidget): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False - if self.mainConf.showGUI and not alreadyAsked: + if self.mainConf.blockGUI and not alreadyAsked: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName @@ -439,7 +439,7 @@ class GuiProjectTree(QTreeWidget): # The file is not already in the trash folder, so we # move it there. doTrash = False - if self.mainConf.showGUI and askForTrash: + if self.mainConf.blockGUI and askForTrash: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 27767916..91391ee3 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -325,7 +325,7 @@ class GuiWritingStats(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog saveTo = QFileDialog.getSaveFileName( @@ -379,7 +379,7 @@ class GuiWritingStats(QDialog): errMsg = str(e) # Report to user - if self.mainConf.showGUI: + if self.mainConf.blockGUI: if wSuccess: self.theParent.makeAlert( "%s file successfully written to:
%s" % ( diff --git a/nw/guimain.py b/nw/guimain.py index 0da4f5a0..2182ab9c 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -195,7 +195,7 @@ class GuiMain(QMainWindow): self.setStatus = self.statusBar.setStatus self.setProjectStatus = self.statusBar.setProjectStatus - if self.mainConf.showGUI: + if self.mainConf.blockGUI: self.show() # Check that config loaded fine @@ -250,7 +250,7 @@ class GuiMain(QMainWindow): projects from a cache of recently opened projects, or provide a browse button for projects not yet cached. """ - if not self.mainConf.showGUI: + if not self.mainConf.blockGUI: return False dlgProj = GuiProjectLoad(self) @@ -275,7 +275,7 @@ class GuiMain(QMainWindow): ) return False - if projData is None and self.mainConf.showGUI: + if projData is None and self.mainConf.blockGUI: projData = self.newProjectDialog() if projData is None: @@ -316,7 +316,7 @@ class GuiMain(QMainWindow): # There is no project loaded, everything OK return True - if self.mainConf.showGUI and not isYes: + if self.mainConf.blockGUI and not isYes: msgBox = QMessageBox() msgRes = msgBox.question( self, "Close Project", "Save changes and close current project?" @@ -332,7 +332,7 @@ class GuiMain(QMainWindow): doBackup = False if self.theProject.doBackup and self.mainConf.backupOnClose: doBackup = True - if self.mainConf.showGUI and self.mainConf.askBeforeBackup: + if self.mainConf.blockGUI and self.mainConf.askBeforeBackup: msgBox = QMessageBox() msgRes = msgBox.question( self, "Backup Project", "Backup current project?" @@ -379,7 +379,7 @@ class GuiMain(QMainWindow): # reason handled by the project class. return False - if self.mainConf.showGUI: + if self.mainConf.blockGUI: try: lockDetails = ( "

The project was locked by the computer " @@ -611,7 +611,7 @@ class GuiMain(QMainWindow): return False if not self.docEditor.isEmpty(): - if self.mainConf.showGUI: + if self.mainConf.blockGUI: msgBox = QMessageBox() msgRes = msgBox.question(self, "Import Document", ( "Importing the file will overwrite the current content of the document. " @@ -629,7 +629,7 @@ class GuiMain(QMainWindow): def mergeDocuments(self): """Merge multiple documents to one single new document. """ - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgMerge = GuiDocMerge(self, self.theProject) dlgMerge.exec_() return True @@ -637,7 +637,7 @@ class GuiMain(QMainWindow): def splitDocument(self): """Split a single document into multiple documents. """ - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgSplit = GuiDocSplit(self, self.theProject) dlgSplit.exec_() return True @@ -684,7 +684,7 @@ class GuiMain(QMainWindow): return logger.verbose("Requesting change to item %s" % tHandle) - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgProj = GuiItemEditor(self, self.theProject, tHandle) if dlgProj.exec_(): self.treeView.setTreeItemValues(tHandle) @@ -745,7 +745,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() - if self.mainConf.showGUI and not beQuiet: + if self.mainConf.blockGUI and not beQuiet: self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO) return True @@ -830,7 +830,7 @@ class GuiMain(QMainWindow): def showAboutNWDialog(self): """Show the about dialog for novelWriter. """ - if self.mainConf.showGUI: + if self.mainConf.blockGUI: dlgAbout = GuiAbout(self) dlgAbout.exec_() return True @@ -838,7 +838,7 @@ class GuiMain(QMainWindow): def showAboutQtDialog(self): """Show the about dialog for Qt. """ - if self.mainConf.showGUI: + if self.mainConf.blockGUI: msgBox = QMessageBox() msgBox.aboutQt(self, "About Qt") return True @@ -870,7 +870,7 @@ class GuiMain(QMainWindow): logger.error(msgLine) # Popup - if self.mainConf.showGUI: + if self.mainConf.blockGUI: msgBox = QMessageBox() if theLevel == nwAlert.INFO: msgBox.information(self, "Information", popMsg) @@ -901,7 +901,7 @@ class GuiMain(QMainWindow): def closeMain(self): """Save everything, and close novelWriter. """ - if self.mainConf.showGUI and self.hasProject: + if self.mainConf.blockGUI and self.hasProject: msgBox = QMessageBox() msgRes = msgBox.question( self, "Exit", "Do you want to save changes and exit?" diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 07f64e8b..8acb3a9c 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -779,7 +779,7 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): nwPrefs.show() # Override Config - tmpConf.showGUI = False + tmpConf.blockGUI = False tmpConf.confPath = nwMinimal nwGUI.mainConf = tmpConf nwPrefs.mainConf = tmpConf From 3f30923d123e55cc5b8261222853cca94ca1e3e5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 22 Sep 2020 23:01:09 +0200 Subject: [PATCH 06/51] Add objectName to all dialogs and made some other minor fixes --- nw/config.py | 10 +++++----- nw/gui/about.py | 1 + nw/gui/build.py | 1 + nw/gui/docmerge.py | 1 + nw/gui/docsplit.py | 1 + nw/gui/itemeditor.py | 1 + nw/gui/preferences.py | 1 + nw/gui/projload.py | 1 + nw/gui/projsettings.py | 1 + nw/gui/projwizard.py | 1 + nw/gui/writingstats.py | 1 + nw/guimain.py | 3 ++- tests/test_dialogs.py | 4 +++- 13 files changed, 20 insertions(+), 7 deletions(-) diff --git a/nw/config.py b/nw/config.py index 9af86619..9cb62e28 100644 --- a/nw/config.py +++ b/nw/config.py @@ -54,7 +54,6 @@ class Config: # Set Application Variables self.appName = "novelWriter" self.appHandle = self.appName.lower() - self.cmdOpen = None # Debug Settings self.blockGUI = True # Allow blocking the GUI (disabled for testing) @@ -65,6 +64,7 @@ class Config: self.errData = [] # List of error messages # Set Paths + self.cmdOpen = None # Path from command line for project to be opened on launch self.confPath = None # Folder where the config is saved self.confFile = None # The config file name self.dataPath = None # Folder where app data is stored @@ -87,11 +87,11 @@ class Config: self.guiTheme = "default" self.guiSyntax = "default_light" self.guiIcons = "typicons_colour_light" - self.guiDark = False - self.guiLang = "en" # Hardcoded for now since the GUI is only in English - self.guiFont = "" + self.guiDark = False # Load icons for dark backgrounds, if available + self.guiLang = "en" # Hardcoded for now since the GUI is only in English + self.guiFont = "" # Defaults to system defualt font self.guiFontSize = 11 - self.guiScale = 1.0 # Set automatically by Theme class + self.guiScale = 1.0 # Set automatically by Theme class ## Sizes self.winGeometry = [1100, 650] diff --git a/nw/gui/about.py b/nw/gui/about.py index 6a25af89..4c1e5539 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -45,6 +45,7 @@ class GuiAbout(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiAbout ...") + self.setObjectName("GuiAbout") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/build.py b/nw/gui/build.py index 155c6596..6c9f5f0e 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -68,6 +68,7 @@ class GuiBuildNovel(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiBuildNovel ...") + self.setObjectName("GuiBuildNovel") self.mainConf = nw.CONFIG self.theProject = theProject diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index d8650ecb..95c4b9de 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -46,6 +46,7 @@ class GuiDocMerge(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiDocMerge ...") + self.setObjectName("GuiDocMerge") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 8c3c4897..6a7ef7b3 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -46,6 +46,7 @@ class GuiDocSplit(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiDocSplit ...") + self.setObjectName("GuiDocSplit") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/itemeditor.py b/nw/gui/itemeditor.py index 13024641..d2a0c741 100644 --- a/nw/gui/itemeditor.py +++ b/nw/gui/itemeditor.py @@ -44,6 +44,7 @@ class GuiItemEditor(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiItemEditor ...") + self.setObjectName("GuiItemEditor") self.mainConf = nw.CONFIG self.theProject = theProject diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index eca12735..6b9af4b3 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -48,6 +48,7 @@ class GuiPreferences(PagedDialog): PagedDialog.__init__(self, theParent) logger.debug("Initialising GuiPreferences ...") + self.setObjectName("GuiPreferences") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/projload.py b/nw/gui/projload.py index c21fc84c..29cafe10 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -58,6 +58,7 @@ class GuiProjectLoad(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiProjectLoad ...") + self.setObjectName("GuiProjectLoad") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index a00d345e..f3ea6a7e 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -47,6 +47,7 @@ class GuiProjectSettings(PagedDialog): PagedDialog.__init__(self, theParent) logger.debug("Initialising GuiProjectSettings ...") + self.setObjectName("GuiProjectSettings") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/projwizard.py b/nw/gui/projwizard.py index bfadbcea..22a0ef77 100644 --- a/nw/gui/projwizard.py +++ b/nw/gui/projwizard.py @@ -55,6 +55,7 @@ class GuiProjectWizard(QWizard): QWizard.__init__(self, theParent) logger.debug("Initialising GuiProjectWizard ...") + self.setObjectName("GuiProjectWizard") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 91391ee3..ad4f8879 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -58,6 +58,7 @@ class GuiWritingStats(QDialog): QDialog.__init__(self, theParent) logger.debug("Initialising GuiWritingStats ...") + self.setObjectName("GuiWritingStats") self.mainConf = nw.CONFIG self.theParent = theParent diff --git a/nw/guimain.py b/nw/guimain.py index 2182ab9c..be0a1c81 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -686,7 +686,8 @@ class GuiMain(QMainWindow): logger.verbose("Requesting change to item %s" % tHandle) if self.mainConf.blockGUI: dlgProj = GuiItemEditor(self, self.theProject, tHandle) - if dlgProj.exec_(): + dlgProj.exec_() + if dlgProj.result() == QDialog.Accepted: self.treeView.setTreeItemValues(tHandle) self.treeMeta.updateViewBox(tHandle) self.docEditor.updateDocInfo(tHandle) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 8acb3a9c..55ba5bf1 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -5,10 +5,12 @@ import nw import pytest import json + from shutil import copyfile from nwtools import cmpFiles from os import path + from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialogButtonBox, QTreeWidgetItem @@ -891,7 +893,6 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): assert not tabAutoRep.autoReplaceDots.isEnabled() # Save and Check Config - # qtbot.stopForInteraction() qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) assert tmpConf.confChanged @@ -901,6 +902,7 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): assert nwGUI.mainConf.saveConfig() + # qtbot.stopForInteraction() nwGUI.closeMain() refConf = path.join(nwRef, "novelwriter_prefs.conf") From 67f8a36272a07508e28b78c89e765c4fef2377d3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 22 Sep 2020 23:13:46 +0200 Subject: [PATCH 07/51] Change copyright year to year-range --- nw/__init__.py | 2 +- nw/common.py | 2 +- nw/config.py | 2 +- nw/constants/constants.py | 2 +- nw/constants/enum.py | 2 +- nw/constants/iso.py | 2 +- nw/core/document.py | 2 +- nw/core/index.py | 2 +- nw/core/item.py | 2 +- nw/core/options.py | 2 +- nw/core/project.py | 2 +- nw/core/spellcheck.py | 2 +- nw/core/status.py | 2 +- nw/core/tohtml.py | 2 +- nw/core/tokenizer.py | 2 +- nw/core/tools.py | 2 +- nw/core/tree.py | 2 +- nw/error.py | 2 +- nw/gui/__init__.py | 2 +- nw/gui/about.py | 2 +- nw/gui/{build.py => buildnovel.py} | 2 +- nw/gui/custom.py | 2 +- nw/gui/doceditor.py | 2 +- nw/gui/dochighlight.py | 2 +- nw/gui/docmerge.py | 2 +- nw/gui/docsplit.py | 2 +- nw/gui/docviewer.py | 2 +- nw/gui/itemdetails.py | 2 +- nw/gui/itemeditor.py | 2 +- nw/gui/mainmenu.py | 2 +- nw/gui/outline.py | 2 +- nw/gui/outlinedetails.py | 2 +- nw/gui/preferences.py | 2 +- nw/gui/projload.py | 2 +- nw/gui/projsettings.py | 2 +- nw/gui/projtree.py | 2 +- nw/gui/projwizard.py | 2 +- nw/gui/statusbar.py | 2 +- nw/gui/theme.py | 2 +- nw/gui/writingstats.py | 2 +- nw/guimain.py | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) rename nw/gui/{build.py => buildnovel.py} (99%) diff --git a/nw/__init__.py b/nw/__init__.py index b43c064a..95f15281 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -9,7 +9,7 @@ Created: 2018-09-22 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/common.py b/nw/common.py index 04a39bf5..12f140ef 100644 --- a/nw/common.py +++ b/nw/common.py @@ -9,7 +9,7 @@ Created: 2019-05-12 [0.1.0] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/config.py b/nw/config.py index 9cb62e28..93141fa5 100644 --- a/nw/config.py +++ b/nw/config.py @@ -9,7 +9,7 @@ Created: 2018-09-22 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 8e2a7d66..358b860e 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -9,7 +9,7 @@ Created: 2019-04-28 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/constants/enum.py b/nw/constants/enum.py index d7c3bde2..e1bcfb46 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -9,7 +9,7 @@ Created: 2018-11-02 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/constants/iso.py b/nw/constants/iso.py index b4985857..c3b663a2 100644 --- a/nw/constants/iso.py +++ b/nw/constants/iso.py @@ -9,7 +9,7 @@ Created: 2019-11-05 [0.4.0] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/document.py b/nw/core/document.py index 45df4cc1..4c0a5542 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -9,7 +9,7 @@ Created: 2018-09-29 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/index.py b/nw/core/index.py index 29126bed..84c02a1f 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -9,7 +9,7 @@ Created: 2019-05-27 [0.1.4] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/item.py b/nw/core/item.py index 6e8048f1..702287dd 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -9,7 +9,7 @@ Created: 2018-10-27 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/options.py b/nw/core/options.py index 314a75c6..7ffa2cdb 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -10,7 +10,7 @@ Rewritten: 2020-02-19 [0.4.5] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/project.py b/nw/core/project.py index 442e76c7..6ac212a1 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -9,7 +9,7 @@ Created: 2018-09-29 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index a3a51db6..9c3a581c 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -9,7 +9,7 @@ Created: 2019-06-11 [0.1.5] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/status.py b/nw/core/status.py index c495e93c..a2e5b92a 100644 --- a/nw/core/status.py +++ b/nw/core/status.py @@ -9,7 +9,7 @@ Created: 2019-05-19 [0.1.3] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 7190bb9e..3e98fc1f 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -9,7 +9,7 @@ Created: 2019-05-07 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index f1c6a988..8a14f0eb 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -9,7 +9,7 @@ Created: 2019-05-05 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/tools.py b/nw/core/tools.py index ff1976e9..c5ef9eed 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -11,7 +11,7 @@ Merged: 2020-05-08 [0.4.5] All of the above into this file This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/core/tree.py b/nw/core/tree.py index 820d12d8..c7939c3b 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -9,7 +9,7 @@ Created: 2020-05-07 [0.4.5] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/error.py b/nw/error.py index 00abc818..fee8fab6 100644 --- a/nw/error.py +++ b/nw/error.py @@ -9,7 +9,7 @@ Created: 2020-08-02 [0.10.2] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index cc2933e7..8d413f61 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from nw.gui.about import GuiAbout -from nw.gui.build import GuiBuildNovel +from nw.gui.buildnovel import GuiBuildNovel from nw.gui.doceditor import GuiDocEditor from nw.gui.docmerge import GuiDocMerge from nw.gui.docsplit import GuiDocSplit diff --git a/nw/gui/about.py b/nw/gui/about.py index 4c1e5539..64730c25 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -9,7 +9,7 @@ Created: 2020-05-21 [0.5.2] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/build.py b/nw/gui/buildnovel.py similarity index 99% rename from nw/gui/build.py rename to nw/gui/buildnovel.py index 6c9f5f0e..5c6af5fc 100644 --- a/nw/gui/build.py +++ b/nw/gui/buildnovel.py @@ -9,7 +9,7 @@ Created: 2020-05-09 [0.5] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/custom.py b/nw/gui/custom.py index 051fa1f3..ea557b51 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -11,7 +11,7 @@ Created: 2020-05-17 [0.5.1] PagedDialog This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index ed36cfdf..ecc51a1c 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -14,7 +14,7 @@ Created: 2020-06-27 [0.10.0] GuiDocEditFooter This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index a374c9a6..786fa859 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -9,7 +9,7 @@ Created: 2019-04-06 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index 95c4b9de..09b18b4b 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -9,7 +9,7 @@ Created: 2020-01-23 [0.4.3] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 6a7ef7b3..c7fc5300 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -9,7 +9,7 @@ Created: 2020-02-01 [0.4.3] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 09f83719..da8a7458 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -13,7 +13,7 @@ Created: 2020-09-08 [1.0b1] GuiDocViewHistory This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py index f1788f06..d5a98254 100644 --- a/nw/gui/itemdetails.py +++ b/nw/gui/itemdetails.py @@ -9,7 +9,7 @@ Created: 2019-04-24 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/itemeditor.py b/nw/gui/itemeditor.py index d2a0c741..42eef5eb 100644 --- a/nw/gui/itemeditor.py +++ b/nw/gui/itemeditor.py @@ -9,7 +9,7 @@ Created: 2019-04-27 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index f59807d3..214446a9 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -9,7 +9,7 @@ Created: 2019-04-27 [0.0.1] (Split from winmain) This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/outline.py b/nw/gui/outline.py index a359b06c..62335934 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -9,7 +9,7 @@ Created: 2019-11-16 [0.4.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 6360a321..95a6de1b 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -9,7 +9,7 @@ Created: 2020-06-02 [0.7.0] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 6b9af4b3..b2e175f5 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -9,7 +9,7 @@ Created: 2019-06-10 [0.1.5] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 29cafe10..65dac1d4 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -9,7 +9,7 @@ Created: 2020-02-26 [0.4.5] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index f3ea6a7e..48a73ab3 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -9,7 +9,7 @@ Created: 2018-09-29 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index db1aa626..2f7833a5 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -10,7 +10,7 @@ Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/projwizard.py b/nw/gui/projwizard.py index 22a0ef77..ec74a184 100644 --- a/nw/gui/projwizard.py +++ b/nw/gui/projwizard.py @@ -9,7 +9,7 @@ Created: 2020-07-11 [0.10.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index ab5fe23a..61cf0409 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -9,7 +9,7 @@ Created: 2019-04-20 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 78f791db..1173c711 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -10,7 +10,7 @@ Created: 2019-11-08 [0.4.0] GuiIcons This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index ad4f8879..e3419a3e 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -9,7 +9,7 @@ Created: 2019-10-20 [0.3] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 diff --git a/nw/guimain.py b/nw/guimain.py index be0a1c81..3d7f2d0b 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -9,7 +9,7 @@ Created: 2018-09-22 [0.0.1] This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen + Copyright 2018–2020, 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 From 0e3aba3b0d8a0be6255be5c8b9317b1c40bcfdfe Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Sep 2020 21:43:49 +0200 Subject: [PATCH 08/51] Increase docViewer test coverage --- tests/test_gui.py | 156 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 3 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index f437bc76..783119d9 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -10,7 +10,7 @@ from nwtools import cmpFiles from os import path from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import QAction, QTreeWidgetItem +from PyQt5.QtWidgets import qApp, QAction, QTreeWidgetItem from nw.constants import nwItemType, nwUnicode, nwOutline, nwDocAction, nwDocInsert @@ -18,7 +18,7 @@ keyDelay = 2 stepDelay = 20 @pytest.mark.gui -def testMainWindow(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -313,8 +313,158 @@ def testMainWindow(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - nwGUI.closeMain() # qtbot.stopForInteraction() + nwGUI.closeMain() + +@pytest.mark.gui +def testDocViewer(qtbot, nwLipsum, nwTemp): + + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + # Open project + nwGUI.theProject.projTree.setSeed(42) + assert nwGUI.openProject(nwLipsum) + + # Rebuild the index as it isn't automatically copied + assert nwGUI.theIndex.tagIndex == {} + assert nwGUI.theIndex.refIndex == {} + nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) + assert nwGUI.theIndex.tagIndex != {} + assert nwGUI.theIndex.refIndex != {} + + # Select a document in the project tree + assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8") + + # Middle-click the selected item + theItem = nwGUI.treeView._getTreeItem("88243afbe5ed8") + theRect = nwGUI.treeView.visualItemRect(theItem) + qtbot.mouseClick(nwGUI.treeView.viewport(), Qt.MidButton, pos=theRect.center()) + assert nwGUI.docViewer.theHandle == "88243afbe5ed8" + + # Reload the text + origText = nwGUI.docViewer.toPlainText() + nwGUI.docViewer.setPlainText("Oops, all gone!") + nwGUI.docViewer.docHeader._refreshDocument() + assert nwGUI.docViewer.toPlainText() == origText + + # Cursor line + assert not nwGUI.docViewer.setCursorLine("not a number") + assert nwGUI.docViewer.setCursorLine(3) + theCursor = nwGUI.docViewer.textCursor() + assert theCursor.position() == 40 + + # Cursor position + assert not nwGUI.docViewer.setCursorPosition("not a number") + assert nwGUI.docViewer.setCursorPosition(100) + + # Select word + nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) + + qClip = qApp.clipboard() + qClip.clear() + + # Cut + assert nwGUI.docViewer.docAction(nwDocAction.CUT) + assert qClip.text() == "laoreet" + qClip.clear() + + # Copy + assert nwGUI.docViewer.docAction(nwDocAction.COPY) + assert qClip.text() == "laoreet" + qClip.clear() + + # Select Paragraph + assert nwGUI.docViewer.docAction(nwDocAction.SEL_PARA) + theCursor = nwGUI.docViewer.textCursor() + assert theCursor.selectedText() == ( + "Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, " + "eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et " + "mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. " + "Etiam finibus nisi vel mi molestie consectetur." + ) + + # Select All + assert nwGUI.docViewer.docAction(nwDocAction.SEL_ALL) + theCursor = nwGUI.docViewer.textCursor() + assert len(theCursor.selectedText()) == 3061 + + # Other actions + assert not nwGUI.docViewer.docAction(nwDocAction.NO_ACTION) + + # Close document + nwGUI.docViewer.docHeader._closeDocument() + assert nwGUI.docViewer.theHandle is None + + # Action on no document + assert not nwGUI.docViewer.docAction(nwDocAction.COPY) + + # Open again via menu + assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8") + nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) + + # Select "Bod" link + assert nwGUI.docViewer.setCursorPosition(27) + nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) + theRect = nwGUI.docViewer.cursorRect() + qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center()) + assert nwGUI.docViewer.theHandle == "4c4f28287af27" + + # Click mouse nav buttons + qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center()) + assert nwGUI.docViewer.theHandle == "88243afbe5ed8" + qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center()) + assert nwGUI.docViewer.theHandle == "4c4f28287af27" + + # Scroll bar default on empty document + nwGUI.docViewer.clear() + assert nwGUI.docViewer.getScrollPosition() == 0 + nwGUI.docViewer.reloadText() + + # Change document title + nwItem = nwGUI.theProject.projTree["4c4f28287af27"] + nwItem.setName("Test Title") + assert nwItem.itemName == "Test Title" + nwGUI.docViewer.updateDocInfo("4c4f28287af27") + assert nwGUI.docViewer.docHeader.theTitle.text() == "Characters › Test Title" + + # Ttile without full path + nwGUI.mainConf.showFullPath = False + nwGUI.docViewer.updateDocInfo("4c4f28287af27") + assert nwGUI.docViewer.docHeader.theTitle.text() == "Test Title" + nwGUI.mainConf.showFullPath = True + + # Document footer show/hide references + viewState = nwGUI.viewMeta.isVisible() + nwGUI.docViewer.docFooter._doShowHide() + assert nwGUI.viewMeta.isVisible() is not viewState + nwGUI.docViewer.docFooter._doShowHide() + assert nwGUI.viewMeta.isVisible() is viewState + + # Document footer sticky + viewState = nwGUI.docViewer.stickyRef + nwGUI.docViewer.docFooter._doToggleSticky(not viewState) + assert nwGUI.docViewer.stickyRef is not viewState + nwGUI.docViewer.docFooter._doToggleSticky(viewState) + assert nwGUI.docViewer.stickyRef is viewState + + # Document footer show/hide synopsis + assert nwGUI.viewDocument("f96ec11c6a3da") + assert len(nwGUI.docViewer.toPlainText()) == 4315 + nwGUI.docViewer.docFooter._doToggleSynopsis(False) + assert len(nwGUI.docViewer.toPlainText()) == 4099 + + # Document footer show/hide comments + assert nwGUI.viewDocument("846352075de7d") + assert len(nwGUI.docViewer.toPlainText()) == 672 + nwGUI.docViewer.docFooter._doToggleComments(False) + assert len(nwGUI.docViewer.toPlainText()) == 632 + + # qtbot.stopForInteraction() + nwGUI.closeMain() @pytest.mark.gui def testEditFormatMenu(qtbot, nwLipsum, nwTemp): From 3c4c60da4560a7c917b13e698b21e0d85920c90b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Sep 2020 22:10:24 +0200 Subject: [PATCH 09/51] Add a delay after mouse click --- tests/test_gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 783119d9..6d6005c5 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -410,13 +410,13 @@ def testDocViewer(qtbot, nwLipsum, nwTemp): assert nwGUI.docViewer.setCursorPosition(27) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) theRect = nwGUI.docViewer.cursorRect() - qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center()) + qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) assert nwGUI.docViewer.theHandle == "4c4f28287af27" # Click mouse nav buttons - qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center()) + qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center(), delay=100) assert nwGUI.docViewer.theHandle == "88243afbe5ed8" - qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center()) + qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center(), delay=100) assert nwGUI.docViewer.theHandle == "4c4f28287af27" # Scroll bar default on empty document From 19aa3c073951a917a8c44f6801d5605e3cc3f909 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Sep 2020 22:14:56 +0200 Subject: [PATCH 10/51] Don't use the link click path --- tests/test_gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 6d6005c5..fd1f426d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -8,7 +8,7 @@ from shutil import copyfile from nwtools import cmpFiles from os import path -from PyQt5.QtCore import Qt, QPoint +from PyQt5.QtCore import Qt, QUrl, QPoint from PyQt5.QtGui import QTextCursor from PyQt5.QtWidgets import qApp, QAction, QTreeWidgetItem @@ -410,7 +410,8 @@ def testDocViewer(qtbot, nwLipsum, nwTemp): assert nwGUI.docViewer.setCursorPosition(27) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) theRect = nwGUI.docViewer.cursorRect() - qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) + # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) + nwGUI.docViewer._linkClicked(QUrl("#char=Bod")) assert nwGUI.docViewer.theHandle == "4c4f28287af27" # Click mouse nav buttons From ad50ef6e8ca385e30ba4b9237ecda4c709d13c9c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Sep 2020 23:24:15 +0200 Subject: [PATCH 11/51] Fix bug in document section stats --- nw/core/index.py | 2 +- tests/test_gui.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 84c02a1f..b32fd0c4 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -356,7 +356,7 @@ class NWIndex(): # Count words for remaining text after last heading if nTitle > 0: - lastText = "\n".join(theLines[nTitle-1:nLine-1]) + lastText = "\n".join(theLines[nTitle-1:]) self._indexWordCounts(tHandle, isNovel, lastText, nTitle) # Update timestamps for index changes diff --git a/tests/test_gui.py b/tests/test_gui.py index fd1f426d..53bce03a 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1074,9 +1074,9 @@ def testOutline(qtbot, nwLipsum, nwTemp): assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum" assert nwGUI.projMeta.itemValue.text() == "Finished" - assert nwGUI.projMeta.cCValue.text() == "122" - assert nwGUI.projMeta.wCValue.text() == "18" - assert nwGUI.projMeta.pCValue.text() == "2" + assert nwGUI.projMeta.cCValue.text() == "230" + assert nwGUI.projMeta.wCValue.text() == "40" + assert nwGUI.projMeta.pCValue.text() == "3" # Scene One actItem = nwGUI.projView.topLevelItem(1) From 008117fece58b1f1a09e9da635a87228dbd4bc64 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Sep 2020 23:30:59 +0200 Subject: [PATCH 12/51] Remove unused code and reuse a few lines in split/merge tools --- nw/core/tokenizer.py | 26 -------------------------- nw/gui/docmerge.py | 3 +-- nw/gui/docsplit.py | 3 +-- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 8a14f0eb..eb6b98d8 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -122,32 +122,6 @@ class Tokenizer(): return - def clearData(self): - """Clear the data arrays and variables, but not settings, so the class - can be reused for multiple documents. - """ - self.theText = None - self.theHandle = None - self.theItem = None - self.theTokens = None - self.theResult = None - self.theMarkdown = None - self.numChapter = 0 - self.firstScene = False - - self.isNone = False - self.isTitle = False - self.isBook = False - self.isPage = False - self.isPart = False - self.isUnNum = False - self.isChap = False - self.isScene = False - self.isNote = False - self.isNovel = False - - return - ## # Setters ## diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index 09b18b4b..8df427c6 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -136,14 +136,13 @@ class GuiDocMerge(QDialog): self.theParent.treeView.revealTreeItem(nHandle) self.theParent.openDocument(nHandle, doScroll=True) - self.close() + self._doClose() return def _doClose(self): """Close the dialog window without doing anything. """ - logger.verbose("GuiDocMerge close button clicked") self.close() return diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index c7fc5300..802419b8 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -214,14 +214,13 @@ class GuiDocSplit(QDialog): theDoc.clearDocument() self.theParent.treeView.revealTreeItem(nHandle) - self.close() + self._doClose() return def _doClose(self): """Close the dialog window without doing anything. """ - logger.verbose("GuiDocSplit close button clicked") self.optState.saveSettings() self.close() return From e36892ed38e3999406a9164dc14c2b1cf45c2710 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 24 Sep 2020 23:31:13 +0200 Subject: [PATCH 13/51] Improve index class test --- tests/test_project.py | 85 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/test_project.py b/tests/test_project.py index 17eae3b0..7502176a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -229,6 +229,11 @@ def testIndexMeta(nwMinimal, nwDummy): assert wC == 12 # Words in text and title only assert pC == 2 # Paragraphs in text only + # Look up an ivalid handle + theRefs = theIndex.getReferences("Not a handle") + assert theRefs["@pov"] == [] + assert theRefs["@char"] == [] + # The novel file should now refer to Jane as @pov and @char theRefs = theIndex.getReferences(nHandle) assert str(theRefs["@pov"]) == "['Jane']" @@ -238,6 +243,86 @@ def testIndexMeta(nwMinimal, nwDummy): theRefs = theIndex.getBackReferenceList(cHandle) assert str(theRefs) == "{'%s': 'T000001'}" % nHandle + # Get section counts for a novel file + assert theIndex.scanText(nHandle, ( + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + "\n" + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + )) + # Whole document + cC, wC, pC = theIndex.getCounts(nHandle) + assert cC == 124 + assert wC == 24 + assert pC == 4 + + # First part + cC, wC, pC = theIndex.getCounts(nHandle, "T000001") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + # First part + cC, wC, pC = theIndex.getCounts(nHandle, "T000011") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + # Get section counts for a note file + assert theIndex.scanText(cHandle, ( + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + "\n" + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + )) + # Whole document + cC, wC, pC = theIndex.getCounts(cHandle) + assert cC == 124 + assert wC == 24 + assert pC == 4 + + # First part + cC, wC, pC = theIndex.getCounts(cHandle, "T000001") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + # First part + cC, wC, pC = theIndex.getCounts(cHandle, "T000011") + assert cC == 62 + assert wC == 12 + assert pC == 2 + assert theProject.closeProject() @pytest.mark.project From eb66b879c5bd09bb7d99635bec9495cf53da9173 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 13:02:51 +0200 Subject: [PATCH 14/51] Imports cleanup --- codecov.yml | 2 +- nw/constants/iso.py | 4 ---- nw/core/index.py | 2 +- nw/core/project.py | 5 +++-- nw/core/spellcheck.py | 2 +- nw/gui/about.py | 2 +- nw/gui/buildnovel.py | 2 +- nw/gui/custom.py | 2 +- nw/gui/doceditor.py | 2 +- nw/gui/dochighlight.py | 2 +- nw/gui/docmerge.py | 2 +- nw/gui/docsplit.py | 8 +++++--- nw/gui/docviewer.py | 2 +- nw/gui/itemdetails.py | 2 +- nw/gui/itemeditor.py | 2 +- nw/gui/mainmenu.py | 2 +- nw/gui/outline.py | 2 +- nw/gui/outlinedetails.py | 2 +- nw/gui/preferences.py | 2 +- nw/gui/projload.py | 2 +- nw/gui/projsettings.py | 2 +- nw/gui/projtree.py | 2 +- nw/gui/projwizard.py | 2 +- nw/gui/statusbar.py | 2 +- nw/gui/theme.py | 2 +- nw/gui/writingstats.py | 2 +- nw/guimain.py | 12 ++++++------ 27 files changed, 37 insertions(+), 38 deletions(-) diff --git a/codecov.yml b/codecov.yml index a649e6c3..cf6354e4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,7 +4,7 @@ codecov: coverage: precision: 2 round: nearest - range: "70...90" + range: "70...95" status: project: diff --git a/nw/constants/iso.py b/nw/constants/iso.py index c3b663a2..69bd6d23 100644 --- a/nw/constants/iso.py +++ b/nw/constants/iso.py @@ -25,10 +25,6 @@ along with this program. If not, see . """ -import logging - -logger = logging.getLogger(__name__) - class isoLanguage(): ISO_639_1 = { diff --git a/nw/core/index.py b/nw/core/index.py index b32fd0c4..c148abd7 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -25,9 +25,9 @@ along with this program. If not, see . """ +import nw import logging import json -import nw from os import path from time import time diff --git a/nw/core/project.py b/nw/core/project.py index 6ac212a1..43885cce 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path, mkdir, listdir, unlink, rename, rmdir from lxml import etree @@ -41,7 +41,8 @@ from nw.core.document import NWDoc from nw.core.status import NWStatus from nw.core.options import OptionState from nw.common import ( - checkString, checkBool, checkInt, isHandle, formatTimeStamp, makeFileNameSafe + checkString, checkBool, checkInt, isHandle, formatTimeStamp, + makeFileNameSafe ) from nw.constants import ( nwFiles, nwItemType, nwItemClass, nwItemLayout, nwLabels, nwAlert diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 9c3a581c..b51a9a63 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path, listdir from difflib import get_close_matches diff --git a/nw/gui/about.py b/nw/gui/about.py index 64730c25..add97c7c 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path from datetime import datetime diff --git a/nw/gui/buildnovel.py b/nw/gui/buildnovel.py index 5c6af5fc..ad2c3248 100644 --- a/nw/gui/buildnovel.py +++ b/nw/gui/buildnovel.py @@ -25,9 +25,9 @@ along with this program. If not, see . """ +import nw import logging import json -import nw from os import path from time import time diff --git a/nw/gui/custom.py b/nw/gui/custom.py index ea557b51..e4e9f0c9 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -27,8 +27,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics from PyQt5.QtCore import ( diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index ecc51a1c..1410c7b4 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -30,8 +30,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from time import time diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 786fa859..7b95ffeb 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt, QRegularExpression from PyQt5.QtGui import ( diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index 8df427c6..786eb429 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 802419b8..766d84f5 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( @@ -34,9 +34,11 @@ from PyQt5.QtWidgets import ( QListWidgetItem, QDialogButtonBox, QLabel, QMessageBox ) -from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout, nwConst -from nw.gui.custom import QHelpLabel from nw.core import NWDoc +from nw.gui.custom import QHelpLabel +from nw.constants import ( + nwAlert, nwItemType, nwItemClass, nwItemLayout, nwConst +) logger = logging.getLogger(__name__) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index da8a7458..2d24efc1 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -29,8 +29,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot from PyQt5.QtGui import ( diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py index d5a98254..f64d570b 100644 --- a/nw/gui/itemdetails.py +++ b/nw/gui/itemdetails.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QPixmap diff --git a/nw/gui/itemeditor.py b/nw/gui/itemeditor.py index 42eef5eb..95a68c82 100644 --- a/nw/gui/itemeditor.py +++ b/nw/gui/itemeditor.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel, diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 214446a9..5dbbede3 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import QUrl, QProcess from PyQt5.QtGui import QDesktopServices diff --git a/nw/gui/outline.py b/nw/gui/outline.py index 62335934..fcee1d8c 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from time import time diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 95a6de1b..c6372826 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index b2e175f5..121d2fd1 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 65dac1d4..54e40a11 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path from datetime import datetime diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 48a73ab3..f5afa35e 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 2f7833a5..21dea08c 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -26,8 +26,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QIcon diff --git a/nw/gui/projwizard.py b/nw/gui/projwizard.py index ec74a184..475ec907 100644 --- a/nw/gui/projwizard.py +++ b/nw/gui/projwizard.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 61cf0409..914fedae 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from time import time diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 1173c711..9b19ca40 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -26,9 +26,9 @@ along with this program. If not, see . """ +import nw import logging import configparser -import nw from os import path, listdir from math import ceil diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index e3419a3e..c356f7f4 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -25,9 +25,9 @@ along with this program. If not, see . """ +import nw import logging import json -import nw from os import path from datetime import datetime diff --git a/nw/guimain.py b/nw/guimain.py index 3d7f2d0b..06828f2e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ -import logging import nw +import logging from os import path from datetime import datetime @@ -40,11 +40,11 @@ from PyQt5.QtWidgets import ( ) from nw.gui import ( - GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, - GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, - GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, - GuiProjectSettings, GuiProjectTree, GuiWritingStats, GuiProjectWizard, - GuiAbout + GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, + GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, + GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences, + GuiProjectLoad, GuiProjectSettings, GuiProjectTree, GuiProjectWizard, + GuiTheme, GuiWritingStats ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwItemType, nwItemClass, nwAlert From 635a52fd31708b47bf005d1646154a30a0841c21 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 13:08:48 +0200 Subject: [PATCH 15/51] More import and file cleanup --- nw/constants/__init__.py | 18 +++++++++--------- nw/core/__init__.py | 6 +++--- nw/gui/__init__.py | 13 ++++++------- nw/gui/{buildnovel.py => build.py} | 0 4 files changed, 18 insertions(+), 19 deletions(-) rename nw/gui/{buildnovel.py => build.py} (100%) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 7c43df61..1ffe2540 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -9,20 +9,20 @@ from nw.constants.enum import ( ) __all__ = [ - "isoLanguage", "isoCountry", - "nwConst", - "nwRegEx", - "nwFiles", - "nwKeyWords", - "nwLabels", - "nwQuotes", - "nwUnicode", + "isoLanguage", "nwAlert", + "nwConst", "nwDocAction", + "nwDocInsert", + "nwFiles", "nwItemClass", "nwItemLayout", "nwItemType", + "nwKeyWords", + "nwLabels", "nwOutline", - "nwDocInsert", + "nwQuotes", + "nwRegEx", + "nwUnicode", ] diff --git a/nw/core/__init__.py b/nw/core/__init__.py index ede01b73..f5140ebc 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -8,6 +8,9 @@ from nw.core.tohtml import ToHtml from nw.core.tools import countWords, numberToRoman, numberToWord __all__ = [ + "countWords", + "numberToRoman", + "numberToWord", "NWDoc", "NWIndex", "NWProject", @@ -15,7 +18,4 @@ __all__ = [ "NWSpellEnchant", "NWSpellSimple", "ToHtml", - "countWords", - "numberToRoman", - "numberToWord", ] diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 8d413f61..d40d84fc 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from nw.gui.about import GuiAbout -from nw.gui.buildnovel import GuiBuildNovel +from nw.gui.build import GuiBuildNovel from nw.gui.doceditor import GuiDocEditor from nw.gui.docmerge import GuiDocMerge from nw.gui.docsplit import GuiDocSplit @@ -16,9 +16,9 @@ from nw.gui.projload import GuiProjectLoad from nw.gui.projsettings import GuiProjectSettings from nw.gui.projtree import GuiProjectTree from nw.gui.projwizard import GuiProjectWizard -from nw.gui.writingstats import GuiWritingStats from nw.gui.statusbar import GuiMainStatus -from nw.gui.theme import GuiIcons, GuiTheme +from nw.gui.theme import GuiTheme +from nw.gui.writingstats import GuiWritingStats __all__ = [ "GuiAbout", @@ -26,11 +26,12 @@ __all__ = [ "GuiDocEditor", "GuiDocMerge", "GuiDocSplit", - "GuiDocViewer", "GuiDocViewDetails", + "GuiDocViewer", "GuiItemDetails", "GuiItemEditor", "GuiMainMenu", + "GuiMainStatus", "GuiOutline", "GuiOutlineDetails", "GuiPreferences", @@ -38,8 +39,6 @@ __all__ = [ "GuiProjectSettings", "GuiProjectTree", "GuiProjectWizard", - "GuiWritingStats", - "GuiMainStatus", - "GuiIcons", "GuiTheme", + "GuiWritingStats", ] diff --git a/nw/gui/buildnovel.py b/nw/gui/build.py similarity index 100% rename from nw/gui/buildnovel.py rename to nw/gui/build.py From be9253e388d36a14e8a37a224c1b8f0eef92c431 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 13:31:04 +0200 Subject: [PATCH 16/51] Added test for the main function --- tests/test_gui.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_gui.py b/tests/test_gui.py index 53bce03a..3659f34b 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -4,6 +4,8 @@ import nw import pytest +import logging + from shutil import copyfile from nwtools import cmpFiles @@ -17,6 +19,61 @@ from nw.constants import nwItemType, nwUnicode, nwOutline, nwDocAction, nwDocIns keyDelay = 2 stepDelay = 20 +@pytest.mark.gui +def testLaunch(qtbot, nwFuncTemp, nwTemp): + + # Log Levels + nwGUI = nw.main( + ["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + assert nw.logger.getEffectiveLevel() == logging.WARNING + nwGUI.closeMain() + + nwGUI = nw.main( + ["--testmode", "--info", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + assert nw.logger.getEffectiveLevel() == logging.INFO + nwGUI.closeMain() + + nwGUI = nw.main( + ["--testmode", "--debug", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + assert nw.logger.getEffectiveLevel() == logging.DEBUG + nwGUI.closeMain() + + nwGUI = nw.main( + ["--testmode", "--verbose", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + assert nw.logger.getEffectiveLevel() == 5 + nwGUI.closeMain() + + # Log file + logFile = path.join(nwTemp, "logFile.log") + bakFile = path.join(nwTemp, "logFile.log.bak") + + nwGUI = nw.main( + ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + assert path.isfile(logFile) + + nwGUI = nw.main( + ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + assert path.isfile(bakFile) + assert path.isfile(logFile) + nwGUI.closeMain() + + # Other options + with pytest.raises(SystemExit): + nwGUI = nw.main( + ["--testmode", "--help", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + + with pytest.raises(SystemExit): + nwGUI = nw.main( + ["--testmode", "--invalid", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + @pytest.mark.gui def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): From 3c21a39ceebc8b418485b97811363bc30f25ac03 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 13:49:08 +0200 Subject: [PATCH 17/51] Extended build tool test --- nw/gui/build.py | 4 +- tests/reference/build/3H_LoremIpsum.json | 71 +++++++++++ tests/reference/build/3M_LoremIpsum.json | 150 +++++++++++++++++++++++ tests/test_dialogs.py | 49 +++++++- 4 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 tests/reference/build/3H_LoremIpsum.json create mode 100644 tests/reference/build/3M_LoremIpsum.json diff --git a/nw/gui/build.py b/nw/gui/build.py index ad2c3248..52b32ddb 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -468,7 +468,7 @@ class GuiBuildNovel(QDialog): self.buildProgress.setMaximum(len(self.theProject.projTree)) self.buildProgress.setValue(0) - tStart = time() + tStart = int(time()) self.htmlText = [] self.htmlStyle = [] @@ -511,7 +511,7 @@ class GuiBuildNovel(QDialog): # Update progress bar, also for skipped items self.buildProgress.setValue(nItt+1) - tEnd = time() + tEnd = int(time()) logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) self.htmlStyle = makeHtml.getStyleSheet() self.buildTime = tEnd diff --git a/tests/reference/build/3H_LoremIpsum.json b/tests/reference/build/3H_LoremIpsum.json new file mode 100644 index 00000000..98c6aecc --- /dev/null +++ b/tests/reference/build/3H_LoremIpsum.json @@ -0,0 +1,71 @@ +{ + "meta": { + "workingTitle": "Lorem Ipsum", + "novelTitle": "Lorem Ipsum", + "authors": [ + "lipsum.com" + ], + "buildTime": 1601120680 + }, + "text": { + "css": [ + "p {text-align: justify;}", + "h1, h2 {color: rgb(66, 113, 174);}", + "h3, h4 {color: rgb(50, 50, 50);}", + "h1, h2, h3, h4 {page-break-after: avoid;}", + "a {color: rgb(66, 113, 174);}", + ".title {font-size: 2.5em;}", + ".tags {color: rgb(245, 135, 31); font-weight: bold;}", + ".break {text-align: left;}", + ".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}", + ".skip {margin-top: 1em; margin-bottom: 1em;}", + ".synopsis {font-style: italic;}", + ".comment {font-style: italic; color: rgb(100, 100, 100);}" + ], + "html": [ + [ + "

Lorem Ipsum

" + ], + [ + "" + ], + [ + "

Prologue

", + "

Synopsis: Explanation from the lipsum.com website.

" + ], + [ + "

Act One

" + ], + [ + "

Chapter One: Chapter One

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.

" + ], + [ + "

Scene 1: Scene One

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.

", + "

Section: Scene One, Section Two

" + ], + [ + "

Scene 2: Scene Two

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.

", + "

Section: Scene Two, Section Two

" + ], + [ + "

Chapter Two: Chapter Two

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.

" + ], + [ + "

Scene 3: Scene Three

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

" + ], + [ + "

Scene 4: Scene Four

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.

" + ], + [ + "

Scene 5: Scene Five

", + "
Point of View: Bod
Plot: Main
Locations: Europe

Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.

" + ] + ] + } +} \ No newline at end of file diff --git a/tests/reference/build/3M_LoremIpsum.json b/tests/reference/build/3M_LoremIpsum.json new file mode 100644 index 00000000..9c357d54 --- /dev/null +++ b/tests/reference/build/3M_LoremIpsum.json @@ -0,0 +1,150 @@ +{ + "meta": { + "workingTitle": "Lorem Ipsum", + "novelTitle": "Lorem Ipsum", + "authors": [ + "lipsum.com" + ], + "buildTime": 1601120788 + }, + "text": { + "nwd": [ + [ + "# Lorem Ipsum", + "", + "", + "", + "", + "" + ], + [ + "", + "", + "", + "" + ], + [ + "## Prologue", + "", + "% Synopsis:Explanation from the lipsum.com website.", + "", + "", + "" + ], + [ + "# Act One", + "", + "", + "" + ], + [ + "## Chapter One", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.", + "", + "", + "" + ], + [ + "### Scene One", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.", + "", + "", + "", + "#### Scene One, Section Two", + "", + "", + "", + "" + ], + [ + "### Scene Two", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. ", + "", + "", + "", + "", + "#### Scene Two, Section Two", + "", + "", + "", + "", + "" + ], + [ + "## Chapter Two", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.", + "", + "", + "" + ], + [ + "### Scene Three", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.", + "", + "", + "", + "", + "", + "" + ], + [ + "### Scene Four", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.", + "", + "", + "", + "", + "", + "", + "", + "" + ], + [ + "### Scene Five", + "", + "@pov: Bod", + "@plot: Main", + "@location: Europe", + "", + "% Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "", + "", + "", + "", + "", + "", + "" + ] + ] + } +} \ No newline at end of file diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 55ba5bf1..f39700de 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -440,23 +440,66 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp): qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) + # Save files that can be compared assert nwBuild._saveDocument(nwBuild.FMT_NWD) - assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = path.join(nwLipsum, "Lorem Ipsum.nwd") testFile = path.join(nwTempBuild, "3_LoremIpsum.nwd") refFile = path.join(nwRef, "build", "3_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) + assert nwBuild._saveDocument(nwBuild.FMT_HTM) projFile = path.join(nwLipsum, "Lorem Ipsum.htm") testFile = path.join(nwTempBuild, "3_LoremIpsum.htm") refFile = path.join(nwRef, "build", "3_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - # qtbot.stopForInteraction() + # Check the JSON files too at this stage + assert nwBuild._saveDocument(nwBuild.FMT_JSON_H) + projFile = path.join(nwLipsum, "Lorem Ipsum.json") + testFile = path.join(nwTempBuild, "3H_LoremIpsum.json") + refFile = path.join(nwRef, "build", "3H_LoremIpsum.json") + copyfile(projFile, testFile) + assert cmpFiles(testFile, refFile, [8]) + + assert nwBuild._saveDocument(nwBuild.FMT_JSON_M) + projFile = path.join(nwLipsum, "Lorem Ipsum.json") + testFile = path.join(nwTempBuild, "3M_LoremIpsum.json") + refFile = path.join(nwRef, "build", "3M_LoremIpsum.json") + copyfile(projFile, testFile) + assert cmpFiles(testFile, refFile, [8]) + + # Save other file types handled by Qt + # We assume the export itself by the Qt library works, so we just + # check that novelWriter successfully writes the files. + assert nwBuild._saveDocument(nwBuild.FMT_ODT) + assert nwBuild._saveDocument(nwBuild.FMT_PDF) + assert nwBuild._saveDocument(nwBuild.FMT_MD) + assert nwBuild._saveDocument(nwBuild.FMT_TXT) + assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.odt")) + assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.pdf")) + assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.md")) + assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.txt")) + + # Close the build tool + htmlText = nwBuild.htmlText + htmlStyle = nwBuild.htmlStyle + nwdText = nwBuild.nwdText + buildTime = nwBuild.buildTime nwBuild._doClose() + + # Re-open build dialog from cahce + nwBuild = GuiBuildNovel(nwGUI, nwGUI.theProject) + + assert nwBuild.htmlText == htmlText + assert nwBuild.htmlStyle == htmlStyle + assert nwBuild.nwdText == nwdText + assert nwBuild.buildTime == buildTime + + nwBuild._doClose() + + # qtbot.stopForInteraction() nwGUI.closeMain() @pytest.mark.gui From ee14dd6e2dd8b60ec6d29065c7ba56fa36e05dbe Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 15:25:30 +0200 Subject: [PATCH 18/51] Added better coverage of themes class --- nw/gui/theme.py | 3 +- tests/test_gui.py | 172 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 4 deletions(-) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 9b19ca40..3c67fb8c 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -34,7 +34,6 @@ from os import path, listdir from math import ceil from PyQt5.QtCore import Qt -from PyQt5.QtSvg import QSvgWidget from PyQt5.QtWidgets import QStyle, qApp from PyQt5.QtGui import ( QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap @@ -670,7 +669,7 @@ class GuiIcons: """ if decoKey not in self.DECO_MAP: logger.error("Decoration with name '%s' does not exist" % decoKey) - return QSvgWidget() + return QPixmap() imgPath = path.join( self.mainConf.assetPath, "images", self.DECO_MAP[decoKey] diff --git a/tests/test_gui.py b/tests/test_gui.py index 3659f34b..58c59efb 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -11,8 +11,8 @@ from nwtools import cmpFiles from os import path from PyQt5.QtCore import Qt, QUrl, QPoint -from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import qApp, QAction, QTreeWidgetItem +from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon +from PyQt5.QtWidgets import qApp, QAction, QTreeWidgetItem, QStyle from nw.constants import nwItemType, nwUnicode, nwOutline, nwDocAction, nwDocInsert @@ -28,24 +28,28 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): ) assert nw.logger.getEffectiveLevel() == logging.WARNING nwGUI.closeMain() + nwGUI.close() nwGUI = nw.main( ["--testmode", "--info", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert nw.logger.getEffectiveLevel() == logging.INFO nwGUI.closeMain() + nwGUI.close() nwGUI = nw.main( ["--testmode", "--debug", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert nw.logger.getEffectiveLevel() == logging.DEBUG nwGUI.closeMain() + nwGUI.close() nwGUI = nw.main( ["--testmode", "--verbose", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert nw.logger.getEffectiveLevel() == 5 nwGUI.closeMain() + nwGUI.close() # Log file logFile = path.join(nwTemp, "logFile.log") @@ -62,17 +66,22 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): assert path.isfile(bakFile) assert path.isfile(logFile) nwGUI.closeMain() + nwGUI.close() # Other options with pytest.raises(SystemExit): nwGUI = nw.main( ["--testmode", "--help", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) + nwGUI.closeMain() + nwGUI.close() with pytest.raises(SystemExit): nwGUI = nw.main( ["--testmode", "--invalid", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) + nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): @@ -372,6 +381,7 @@ def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testDocViewer(qtbot, nwLipsum, nwTemp): @@ -523,6 +533,7 @@ def testDocViewer(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testEditFormatMenu(qtbot, nwLipsum, nwTemp): @@ -759,6 +770,7 @@ def testEditFormatMenu(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testContextMenu(qtbot, nwLipsum, nwTemp): @@ -844,6 +856,7 @@ def testContextMenu(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testInsertMenu(qtbot, nwFuncTemp, nwTemp): @@ -934,6 +947,7 @@ def testInsertMenu(qtbot, nwFuncTemp, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testTextSearch(qtbot, nwLipsum, nwTemp): @@ -1093,6 +1107,7 @@ def testTextSearch(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testOutline(qtbot, nwLipsum, nwTemp): @@ -1153,3 +1168,156 @@ def testOutline(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() + +@pytest.mark.gui +def testThemes(qtbot, nwMinimal, nwTemp): + + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(500) + + # Change Settings + assert nw.CONFIG.confPath == nwMinimal + nw.CONFIG.guiTheme = "default_dark" + nw.CONFIG.guiSyntax = "tomorrow_night_eighties" + nw.CONFIG.guiIcons = "typicons_colour_dark" + nw.CONFIG.guiDark = True + nw.CONFIG.guiFont = "Cantarell" + nw.CONFIG.guiFontSize = 11 + nw.CONFIG.confChanged = True + assert nw.CONFIG.saveConfig() + + nwGUI.closeMain() + nwGUI.close() + del nwGUI + + # Re-open + assert nw.CONFIG.confPath == nwMinimal + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + assert nwGUI.mainConf.confPath == nwMinimal + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(500) + + assert nw.CONFIG.guiTheme == "default_dark" + assert nw.CONFIG.guiSyntax == "tomorrow_night_eighties" + assert nw.CONFIG.guiIcons == "typicons_colour_dark" + assert nw.CONFIG.guiDark is True + assert nw.CONFIG.guiFont == "Cantarell" + assert nw.CONFIG.guiFontSize == 11 + + # Check GUI Colours + thePalette = nwGUI.palette() + assert thePalette.window().color() == QColor(54, 54, 54) + assert thePalette.windowText().color() == QColor(174, 174, 174) + assert thePalette.base().color() == QColor(62, 62, 62) + assert thePalette.alternateBase().color() == QColor(67, 67, 67) + assert thePalette.text().color() == QColor(174, 174, 174) + assert thePalette.toolTipBase().color() == QColor(255, 255, 192) + assert thePalette.toolTipText().color() == QColor(21, 21, 13) + assert thePalette.button().color() == QColor(62, 62, 62) + assert thePalette.buttonText().color() == QColor(174, 174, 174) + assert thePalette.brightText().color() == QColor(174, 174, 174) + assert thePalette.highlight().color() == QColor(44, 152, 247) + assert thePalette.highlightedText().color() == QColor(255, 255, 255) + assert thePalette.link().color() == QColor(44, 152, 247) + assert thePalette.linkVisited().color() == QColor(44, 152, 247) + + assert nwGUI.theTheme.treeWCount == [197, 200, 198] + assert nwGUI.theTheme.statNone == [150, 152, 150] + assert nwGUI.theTheme.statSaved == [39, 135, 78] + assert nwGUI.theTheme.statUnsaved == [138, 32, 32] + + # Check Syntax Colours + assert nwGUI.theTheme.colBack == [45, 45, 45] + assert nwGUI.theTheme.colText == [204, 204, 204] + assert nwGUI.theTheme.colLink == [102, 153, 204] + assert nwGUI.theTheme.colHead == [102, 153, 204] + assert nwGUI.theTheme.colHeadH == [102, 153, 204] + assert nwGUI.theTheme.colEmph == [249, 145, 57] + assert nwGUI.theTheme.colDialN == [242, 119, 122] + assert nwGUI.theTheme.colDialD == [153, 204, 153] + assert nwGUI.theTheme.colDialS == [255, 204, 102] + assert nwGUI.theTheme.colComm == [153, 153, 153] + assert nwGUI.theTheme.colKey == [242, 119, 122] + assert nwGUI.theTheme.colVal == [204, 153, 204] + assert nwGUI.theTheme.colSpell == [242, 119, 122] + assert nwGUI.theTheme.colTagErr == [153, 204, 153] + assert nwGUI.theTheme.colRepTag == [102, 204, 204] + assert nwGUI.theTheme.colMod == [249, 145, 57] + + # Test Icon class + theIcons = nwGUI.theTheme.theIcons + nw.CONFIG.guiIcons = "invalid" + assert not theIcons.updateTheme() + nw.CONFIG.guiIcons = "typicons_colour_dark" + assert theIcons.updateTheme() + + # Ask for a non-existent key + anImg = theIcons.loadDecoration("nonsense", 20, 20) + assert isinstance(anImg, QPixmap) + assert anImg.isNull() + + # Add a non-existent file and request it + theIcons.DECO_MAP["nonsense"] = "nofile.jpg" + anImg = theIcons.loadDecoration("nonsense", 20, 20) + assert isinstance(anImg, QPixmap) + assert anImg.isNull() + + # Get a real image, with different size parameters + anImg = theIcons.loadDecoration("wiz-back", 20, None) + assert isinstance(anImg, QPixmap) + assert not anImg.isNull() + assert anImg.width() == 20 + assert anImg.height() >= 56 + + anImg = theIcons.loadDecoration("wiz-back", None, 70) + assert isinstance(anImg, QPixmap) + assert not anImg.isNull() + assert anImg.height() == 70 + assert anImg.width() >= 24 + + anImg = theIcons.loadDecoration("wiz-back", 30, 70) + assert isinstance(anImg, QPixmap) + assert not anImg.isNull() + assert anImg.height() == 70 + assert anImg.width() == 30 + + anImg = theIcons.loadDecoration("wiz-back", None, None) + assert isinstance(anImg, QPixmap) + assert not anImg.isNull() + assert anImg.height() >= 1500 + assert anImg.width() >= 500 + + # Load icons + anIcon = theIcons.getIcon("nonsense") + assert isinstance(anIcon, QIcon) + assert anIcon.isNull() + + anIcon = theIcons.getIcon("novelwriter") + assert isinstance(anIcon, QIcon) + assert not anIcon.isNull() + + # Add dummy icons and test alternative load paths + theIcons.ICON_MAP["testicon1"] = (QStyle.SP_DriveHDIcon, None) + anIcon = theIcons.getIcon("testicon1") + assert isinstance(anIcon, QIcon) + assert not anIcon.isNull() + + theIcons.ICON_MAP["testicon2"] = (None, "drive-harddisk") + anIcon = theIcons.getIcon("testicon2") + assert isinstance(anIcon, QIcon) + assert not anIcon.isNull() + + theIcons.ICON_MAP["testicon3"] = (None, None) + anIcon = theIcons.getIcon("testicon3") + assert isinstance(anIcon, QIcon) + assert anIcon.isNull() + + # qtbot.stopForInteraction() + nwGUI.closeMain() + nwGUI.close() From 711e27f048407d0f0cdfa9f8d4fa6c12457afc57 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 15:30:04 +0200 Subject: [PATCH 19/51] Try to add icon theme to github action setup --- .github/workflows/pytest_cov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest_cov.yml b/.github/workflows/pytest_cov.yml index 1969d200..71fa5417 100644 --- a/.github/workflows/pytest_cov.yml +++ b/.github/workflows/pytest_cov.yml @@ -18,7 +18,7 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default + sudo apt install xvfb libenchant-dev qt5-default breeze-icon-theme - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies @@ -51,7 +51,7 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default + sudo apt install xvfb libenchant-dev qt5-default breeze-icon-theme - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies From 8697604f2b173eb6ae7989604d6b44d74d3685b2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 15:36:11 +0200 Subject: [PATCH 20/51] Drop icon theme dependency in test --- .github/workflows/pytest_cov.yml | 4 ++-- tests/test_gui.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest_cov.yml b/.github/workflows/pytest_cov.yml index 71fa5417..1969d200 100644 --- a/.github/workflows/pytest_cov.yml +++ b/.github/workflows/pytest_cov.yml @@ -18,7 +18,7 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default breeze-icon-theme + sudo apt install xvfb libenchant-dev qt5-default - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies @@ -51,7 +51,7 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default breeze-icon-theme + sudo apt install xvfb libenchant-dev qt5-default - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies diff --git a/tests/test_gui.py b/tests/test_gui.py index 58c59efb..92fe1a8d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1308,10 +1308,9 @@ def testThemes(qtbot, nwMinimal, nwTemp): assert isinstance(anIcon, QIcon) assert not anIcon.isNull() - theIcons.ICON_MAP["testicon2"] = (None, "drive-harddisk") + theIcons.ICON_MAP["testicon2"] = (None, "folder") anIcon = theIcons.getIcon("testicon2") assert isinstance(anIcon, QIcon) - assert not anIcon.isNull() theIcons.ICON_MAP["testicon3"] = (None, None) anIcon = theIcons.getIcon("testicon3") From 31c642b8bc36a72b5b68fc00d400dc36ce83b0d2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 16:10:07 +0200 Subject: [PATCH 21/51] Added test for QuotesDialog --- tests/test_dialogs.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index f39700de..1fceaca1 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -11,14 +11,17 @@ from nwtools import cmpFiles from os import path -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QDialogButtonBox, QTreeWidgetItem +from PyQt5.QtCore import Qt, QItemSelectionModel +from PyQt5.QtWidgets import ( + QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog +) from nw.gui import ( GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel, GuiDocMerge, GuiDocSplit, GuiWritingStats, GuiProjectWizard, GuiProjectLoad, GuiPreferences ) +from nw.gui.custom import QuotesDialog from nw.constants import nwItemType, nwItemLayout, nwItemClass keyDelay = 2 @@ -958,3 +961,33 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): 7, 25, # Fonts (depends in system default) ] assert cmpFiles(testConf, refConf, ignoreLines) + +@pytest.mark.gui +def testQuotesDialog(qtbot, nwMinimal, nwTemp): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwQuot = QuotesDialog(nwGUI) + nwQuot.show() + + lastItem = "" + for i in range(nwQuot.listBox.count()): + anItem = nwQuot.listBox.item(i) + assert isinstance(anItem, QListWidgetItem) + nwQuot.listBox.clearSelection() + nwQuot.listBox.setCurrentItem(anItem, QItemSelectionModel.Select) + lastItem = anItem.text()[2] + assert nwQuot.previewLabel.text() == lastItem + + nwQuot._doAccept() + assert nwQuot.result() == QDialog.Accepted + assert nwQuot.selectedQuote == lastItem + + # qtbot.stopForInteraction() + nwQuot._doReject() + nwQuot.close() + nwGUI.closeMain() + nwGUI.close() From fa72b9b0d0240cfce72f914f9cd9979e799cd125 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 16:48:43 +0200 Subject: [PATCH 22/51] Added test to convert older project to new format --- nw/core/project.py | 4 +- tests/conftest.py | 13 ++ tests/oldproj/data_1/9752e7f9d8af_main.nwd | 4 + tests/oldproj/data_7/ff63b8afc4cd_main.nwd | 4 + tests/oldproj/data_8/8124a4292d8b_main.nwd | 4 + tests/oldproj/data_9/058ae29f0dfd_main.nwd | 4 + tests/oldproj/data_9/1239bf2f8b69_main.nwd | 4 + tests/oldproj/data_a/764d5acf5a21_main.nwd | 4 + tests/oldproj/data_f/528d831f5b24_main.nwd | 4 + tests/oldproj/meta/sessionInfo.log | 2 + tests/oldproj/meta/tagsIndex.json | 72 ++++++++++ tests/oldproj/nwProject.nwx | 148 +++++++++++++++++++++ tests/test_project.py | 86 +++++++++++- 13 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 tests/oldproj/data_1/9752e7f9d8af_main.nwd create mode 100644 tests/oldproj/data_7/ff63b8afc4cd_main.nwd create mode 100644 tests/oldproj/data_8/8124a4292d8b_main.nwd create mode 100644 tests/oldproj/data_9/058ae29f0dfd_main.nwd create mode 100644 tests/oldproj/data_9/1239bf2f8b69_main.nwd create mode 100644 tests/oldproj/data_a/764d5acf5a21_main.nwd create mode 100644 tests/oldproj/data_f/528d831f5b24_main.nwd create mode 100644 tests/oldproj/meta/sessionInfo.log create mode 100644 tests/oldproj/meta/tagsIndex.json create mode 100644 tests/oldproj/nwProject.nwx diff --git a/nw/core/project.py b/nw/core/project.py index 43885cce..5efce6ad 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -489,7 +489,7 @@ class NWProject(): # parser will lose the autoReplace settings if allowed to # read the file. Introduced in version 0.10. - if fileVersion == "1.0": + if fileVersion == "1.0" and self.mainConf.blockGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( "The project file and data is created by a novelWriter version " @@ -501,7 +501,7 @@ class NWProject(): if msgRes != QMessageBox.Yes: return False - elif fileVersion != "1.1" and fileVersion != "1.2": + elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.blockGUI: self.makeAlert(( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " diff --git a/tests/conftest.py b/tests/conftest.py index 61bd1211..14988efa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -118,3 +118,16 @@ def nwLipsum(nwTemp): if path.isdir(lipsumDir): shutil.rmtree(lipsumDir) return + +@pytest.fixture(scope="function") +def nwOldProj(nwTemp): + testDir = path.dirname(__file__) + oldProjStore = path.join(testDir, "oldproj") + oldProjDir = path.join(nwTemp, "oldproj") + if path.isdir(oldProjDir): + shutil.rmtree(oldProjDir) + shutil.copytree(oldProjStore, oldProjDir) + yield oldProjDir + if path.isdir(oldProjDir): + shutil.rmtree(oldProjDir) + return diff --git a/tests/oldproj/data_1/9752e7f9d8af_main.nwd b/tests/oldproj/data_1/9752e7f9d8af_main.nwd new file mode 100644 index 00000000..5b25ad52 --- /dev/null +++ b/tests/oldproj/data_1/9752e7f9d8af_main.nwd @@ -0,0 +1,4 @@ +### Scene Four + +Scene Four + diff --git a/tests/oldproj/data_7/ff63b8afc4cd_main.nwd b/tests/oldproj/data_7/ff63b8afc4cd_main.nwd new file mode 100644 index 00000000..818712f7 --- /dev/null +++ b/tests/oldproj/data_7/ff63b8afc4cd_main.nwd @@ -0,0 +1,4 @@ +# Antagonist + +Antagonist + diff --git a/tests/oldproj/data_8/8124a4292d8b_main.nwd b/tests/oldproj/data_8/8124a4292d8b_main.nwd new file mode 100644 index 00000000..5fe1d9fe --- /dev/null +++ b/tests/oldproj/data_8/8124a4292d8b_main.nwd @@ -0,0 +1,4 @@ +### Scene Two + +Scene Two + diff --git a/tests/oldproj/data_9/058ae29f0dfd_main.nwd b/tests/oldproj/data_9/058ae29f0dfd_main.nwd new file mode 100644 index 00000000..79e4dc06 --- /dev/null +++ b/tests/oldproj/data_9/058ae29f0dfd_main.nwd @@ -0,0 +1,4 @@ +# Protagonist + +Protagonist + diff --git a/tests/oldproj/data_9/1239bf2f8b69_main.nwd b/tests/oldproj/data_9/1239bf2f8b69_main.nwd new file mode 100644 index 00000000..2d701cd4 --- /dev/null +++ b/tests/oldproj/data_9/1239bf2f8b69_main.nwd @@ -0,0 +1,4 @@ +### Scene Three + +Scene Three + diff --git a/tests/oldproj/data_a/764d5acf5a21_main.nwd b/tests/oldproj/data_a/764d5acf5a21_main.nwd new file mode 100644 index 00000000..7ef7c622 --- /dev/null +++ b/tests/oldproj/data_a/764d5acf5a21_main.nwd @@ -0,0 +1,4 @@ +### Scene Five + +Scene Five + diff --git a/tests/oldproj/data_f/528d831f5b24_main.nwd b/tests/oldproj/data_f/528d831f5b24_main.nwd new file mode 100644 index 00000000..8fecdb8e --- /dev/null +++ b/tests/oldproj/data_f/528d831f5b24_main.nwd @@ -0,0 +1,4 @@ +### Scene One + +Scene One + diff --git a/tests/oldproj/meta/sessionInfo.log b/tests/oldproj/meta/sessionInfo.log new file mode 100644 index 00000000..92da5e9a --- /dev/null +++ b/tests/oldproj/meta/sessionInfo.log @@ -0,0 +1,2 @@ +Start: 2020-09-26 16:13:00 End: 2020-09-26 16:15:54 Words: 24 +Start: 2020-09-26 16:16:28 End: 2020-09-26 16:16:40 Words: -1 diff --git a/tests/oldproj/meta/tagsIndex.json b/tests/oldproj/meta/tagsIndex.json new file mode 100644 index 00000000..d02515f6 --- /dev/null +++ b/tests/oldproj/meta/tagsIndex.json @@ -0,0 +1,72 @@ +{ + "tagIndex": {}, + "refIndex": { + "f528d831f5b24": [], + "88124a4292d8b": [], + "91239bf2f8b69": [], + "19752e7f9d8af": [], + "a764d5acf5a21": [], + "9058ae29f0dfd": [], + "7ff63b8afc4cd": [] + }, + "novelIndex": { + "f528d831f5b24": [ + [ + 1, + 3, + "Scene One", + "SCENE" + ] + ], + "88124a4292d8b": [ + [ + 1, + 3, + "Scene Two", + "SCENE" + ] + ], + "91239bf2f8b69": [ + [ + 1, + 3, + "Scene Three", + "SCENE" + ] + ], + "19752e7f9d8af": [ + [ + 1, + 3, + "Scene Four", + "SCENE" + ] + ], + "a764d5acf5a21": [ + [ + 1, + 3, + "Scene Five", + "SCENE" + ] + ] + }, + "noteIndex": { + "9058ae29f0dfd": [ + [ + 1, + 1, + "Protagonist", + "NOTE" + ] + ], + "7ff63b8afc4cd": [ + [ + 1, + 1, + "Antagonist", + "NOTE" + ] + ] + } +} \ No newline at end of file diff --git a/tests/oldproj/nwProject.nwx b/tests/oldproj/nwProject.nwx new file mode 100644 index 00000000..8ba21098 --- /dev/null +++ b/tests/oldproj/nwProject.nwx @@ -0,0 +1,148 @@ + + + + + + True + + + False + a764d5acf5a21 + None + 23 + + + New + Note + Draft + Finished + + + New + Minor + Major + Main + + + + + Novel + ROOT + NOVEL + New + True + + + Chapter One + FOLDER + NOVEL + New + True + + + Scene One + FILE + NOVEL + New + False + SCENE + 18 + 4 + 1 + 3 + + + Scene Two + FILE + NOVEL + New + False + SCENE + 18 + 4 + 1 + 2 + + + Scene Three + FILE + NOVEL + New + False + SCENE + 22 + 4 + 1 + 2 + + + Scene Four + FILE + NOVEL + New + False + SCENE + 20 + 4 + 1 + 2 + + + Scene Five + FILE + NOVEL + New + False + SCENE + 20 + 4 + 1 + 2 + + + Characters + ROOT + CHARACTER + New + True + + + Protagonist + FILE + CHARACTER + New + False + NOTE + 11 + 1 + 0 + 28 + + + Antagonist + FILE + CHARACTER + New + False + NOTE + 13 + 2 + 1 + 26 + + + Plot + ROOT + PLOT + New + False + + + World + ROOT + WORLD + New + False + + + diff --git a/tests/test_project.py b/tests/test_project.py index 7502176a..be7e425d 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -3,7 +3,7 @@ """ import pytest -from os import path +from os import path, mkdir from shutil import copyfile from nwtools import cmpFiles @@ -577,3 +577,87 @@ def testOrphanedFiles(nwDummy, nwLipsum): assert theProject.saveProject(nwLipsum) assert theProject.closeProject() + +@pytest.mark.project +def testOldProject(nwDummy, nwOldProj): + theProject = NWProject(nwDummy) + theProject.mainConf.blockGUI = False + + # Create dummy files for known legacy files + deleteFiles = [ + path.join(nwOldProj, "cache", "nwProject.nwx.0"), + path.join(nwOldProj, "cache", "nwProject.nwx.1"), + path.join(nwOldProj, "cache", "nwProject.nwx.2"), + path.join(nwOldProj, "cache", "nwProject.nwx.3"), + path.join(nwOldProj, "cache", "nwProject.nwx.4"), + path.join(nwOldProj, "cache", "nwProject.nwx.5"), + path.join(nwOldProj, "cache", "nwProject.nwx.6"), + path.join(nwOldProj, "cache", "nwProject.nwx.7"), + path.join(nwOldProj, "cache", "nwProject.nwx.8"), + path.join(nwOldProj, "cache", "nwProject.nwx.9"), + path.join(nwOldProj, "meta", "mainOptions.json"), + path.join(nwOldProj, "meta", "exportOptions.json"), + path.join(nwOldProj, "meta", "outlineOptions.json"), + path.join(nwOldProj, "meta", "timelineOptions.json"), + path.join(nwOldProj, "meta", "docMergeOptions.json"), + path.join(nwOldProj, "meta", "sessionLogOptions.json"), + ] + + # Add some files that shouldn't be there + deleteFiles.append(path.join(nwOldProj, "data_f", "whatnow.nwd")) + deleteFiles.append(path.join(nwOldProj, "data_f", "whatnow.txt")) + + # Add some folders that shouldn't be there + mkdir(path.join(nwOldProj, "stuff")) + mkdir(path.join(nwOldProj, "data_1", "stuff")) + + for aFile in deleteFiles: + with open(aFile, mode="w+", encoding="utf8") as outFile: + outFile.write("Hi") + for aFile in deleteFiles: + assert path.isfile(aFile) + + # Open project and check that files that are not supposed to be + # there have been removed + assert theProject.openProject(nwOldProj) + for aFile in deleteFiles: + assert not path.isfile(aFile) + + assert not path.isdir(path.join(nwOldProj, "data_1", "stuff")) + assert not path.isdir(path.join(nwOldProj, "data_1")) + assert not path.isdir(path.join(nwOldProj, "data_7")) + assert not path.isdir(path.join(nwOldProj, "data_8")) + assert not path.isdir(path.join(nwOldProj, "data_9")) + assert not path.isdir(path.join(nwOldProj, "data_a")) + assert not path.isdir(path.join(nwOldProj, "data_f")) + + # Check stuff that has been moved + assert path.isdir(path.join(nwOldProj, "junk")) + assert path.isdir(path.join(nwOldProj, "junk", "stuff")) + assert path.isfile(path.join(nwOldProj, "junk", "whatnow.nwd")) + assert path.isfile(path.join(nwOldProj, "junk", "whatnow.txt")) + + # Check that files we want to keep are in the right place + assert path.isdir(path.join(nwOldProj, "cache")) + assert path.isdir(path.join(nwOldProj, "content")) + assert path.isdir(path.join(nwOldProj, "meta")) + + assert path.isfile(path.join(nwOldProj, "content", "f528d831f5b24.nwd")) + assert path.isfile(path.join(nwOldProj, "content", "88124a4292d8b.nwd")) + assert path.isfile(path.join(nwOldProj, "content", "91239bf2f8b69.nwd")) + assert path.isfile(path.join(nwOldProj, "content", "19752e7f9d8af.nwd")) + assert path.isfile(path.join(nwOldProj, "content", "a764d5acf5a21.nwd")) + assert path.isfile(path.join(nwOldProj, "content", "9058ae29f0dfd.nwd")) + assert path.isfile(path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd")) + + assert path.isfile(path.join(nwOldProj, "meta", "tagsIndex.json")) + assert path.isfile(path.join(nwOldProj, "meta", "sessionInfo.log")) + + # Close the project + theProject.closeProject() + + # Check that new files have been created + assert path.isfile(path.join(nwOldProj, "meta", "guiOptions.json")) + assert path.isfile(path.join(nwOldProj, "meta", "sessionStats.log")) + assert path.isfile(path.join(nwOldProj, "ToC.json")) + assert path.isfile(path.join(nwOldProj, "ToC.txt")) From f013662e20fd12742f9e006a4bf867c19c243ab0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 17:17:34 +0200 Subject: [PATCH 23/51] Add test of zipped projects for backup --- tests/test_project.py | 47 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/test_project.py b/tests/test_project.py index be7e425d..9ee14615 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -3,8 +3,9 @@ """ import pytest -from os import path, mkdir +from os import path, mkdir, listdir from shutil import copyfile +from zipfile import ZipFile from nwtools import cmpFiles @@ -661,3 +662,47 @@ def testOldProject(nwDummy, nwOldProj): assert path.isfile(path.join(nwOldProj, "meta", "sessionStats.log")) assert path.isfile(path.join(nwOldProj, "ToC.json")) assert path.isfile(path.join(nwOldProj, "ToC.txt")) + +@pytest.mark.project +def testBackupProject(nwDummy, nwMinimal, nwTemp): + theProject = NWProject(nwDummy) + assert theProject.openProject(nwMinimal) + + # Test faulty settings + # Invalid path + theProject.mainConf.backupPath = None + assert not theProject.zipIt(doNotify=False) + + # Missing project name + theProject.mainConf.backupPath = nwTemp + theProject.projName = "" + assert not theProject.zipIt(doNotify=False) + + # Non-existent folder + theProject.mainConf.backupPath = path.join(nwTemp, "nonexistent") + theProject.projName = "Test Minimal" + assert not theProject.zipIt(doNotify=False) + + # Same folder as project (causes infinite loop in zipping) + theProject.mainConf.backupPath = nwMinimal + assert not theProject.zipIt(doNotify=False) + + # Test correct settings + theProject.mainConf.backupPath = nwTemp + assert theProject.zipIt(doNotify=False) + + theFiles = listdir(path.join(nwTemp, "Test Minimal")) + assert len(theFiles) == 1 + + theZip = theFiles[0] + assert theZip[:12] == "Backup from " + assert theZip[-4:] == ".zip" + + # Extract the archive + with ZipFile(path.join(nwTemp, "Test Minimal", theZip), "r") as inZip: + inZip.extractall(path.join(nwTemp, "extract")) + + # Check that the main project file was restored + assert cmpFiles( + path.join(nwMinimal, "nwProject.nwx"), path.join(nwTemp, "extract", "nwProject.nwx") + ) From efd34ded630973c3f307552f95ed23d03aca0223 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 26 Sep 2020 17:21:54 +0200 Subject: [PATCH 24/51] Fix bug in old project test --- tests/test_project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_project.py b/tests/test_project.py index 9ee14615..1ec42348 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -612,6 +612,8 @@ def testOldProject(nwDummy, nwOldProj): mkdir(path.join(nwOldProj, "stuff")) mkdir(path.join(nwOldProj, "data_1", "stuff")) + # Create dummy files + mkdir(path.join(nwOldProj, "cache")) for aFile in deleteFiles: with open(aFile, mode="w+", encoding="utf8") as outFile: outFile.write("Hi") From bd5c011cd9cbec9c66f94feff2b399a8c9749481 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 27 Sep 2020 00:12:42 +0200 Subject: [PATCH 25/51] Better test coverage of project tree view --- nw/gui/projtree.py | 31 ++++++---- tests/test_gui.py | 140 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 14 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 21dea08c..f4280f30 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -178,6 +178,9 @@ class GuiProjectTree(QTreeWidget): if itemType == nwItemType.ROOT: tHandle = self.theProject.newRoot(nwLabels.CLASS_NAME[itemClass], itemClass) + if tHandle is None: + logger.error("No root item added") + return False else: # If no parent has been selected, make the new file under @@ -236,8 +239,9 @@ class GuiProjectTree(QTreeWidget): return False # Add the new item to the tree - self.revealTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) + if tHandle is not None: + self.revealTreeItem(tHandle, nHandle) + self.theParent.editItem(tHandle) return True @@ -257,7 +261,8 @@ class GuiProjectTree(QTreeWidget): """Move an item up or down in the tree, but only if the treeView has focus. This also applies when the menu is used. """ - if qApp.focusWidget() == self and self.theParent.hasProject: + hasFocus = qApp.focusWidget() == self or not self.mainConf.blockGUI + if hasFocus and self.theParent.hasProject: tHandle = self.getSelectedHandle() tItem = self._getTreeItem(tHandle) @@ -357,14 +362,15 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The Trash folder is already empty.", nwAlert.INFO) return False - msgBox = QMessageBox() - msgRes = msgBox.question( - self, "Empty Trash", "Permanently delete %d file%s from Trash?" % ( - nTrash, "s" if nTrash > 1 else "" + if self.mainConf.blockGUI: + msgBox = QMessageBox() + msgRes = msgBox.question( + self, "Empty Trash", "Permanently delete %d file%s from Trash?" % ( + nTrash, "s" if nTrash > 1 else "" + ) ) - ) - if msgRes != QMessageBox.Yes: - return False + if msgRes != QMessageBox.Yes: + return False logger.verbose("Deleting %d files from Trash" % nTrash) for tHandle in self.getTreeFromHandle(trashHandle): @@ -754,6 +760,7 @@ class GuiProjectTree(QTreeWidget): self.theIndex.reIndexHandle(sHandle) else: + theEvent.ignore() logger.debug("Drag'n'drop of item %s not accepted" % sHandle) self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) @@ -920,14 +927,14 @@ class GuiProjectTree(QTreeWidget): nwItemS.setClass(nwItemD.itemClass) if trItemP is None: logger.error("Failed to find new parent item of %s" % tHandle) - return + return False pHandle = trItemP.data(self.C_NAME, Qt.UserRole) nwItemS.setParent(pHandle) self.setTreeItemValues(tHandle) self._setTreeChanged(True) - return + return True def _setTreeChanged(self, theState): """Set the tree change flag, and propagate to the project. diff --git a/tests/test_gui.py b/tests/test_gui.py index 92fe1a8d..ec24d5ec 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -10,11 +10,13 @@ from shutil import copyfile from nwtools import cmpFiles from os import path -from PyQt5.QtCore import Qt, QUrl, QPoint +from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon from PyQt5.QtWidgets import qApp, QAction, QTreeWidgetItem, QStyle -from nw.constants import nwItemType, nwUnicode, nwOutline, nwDocAction, nwDocInsert +from nw.constants import ( + nwItemType, nwItemClass, nwUnicode, nwOutline, nwDocAction, nwDocInsert +) keyDelay = 2 stepDelay = 20 @@ -535,6 +537,140 @@ def testDocViewer(qtbot, nwLipsum, nwTemp): nwGUI.closeMain() nwGUI.close() +@pytest.mark.gui +def testProjectTree(qtbot, nwMinimal, nwTemp): + + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwGUI.theProject.projTree.setSeed(42) + nwTree = nwGUI.treeView + + # No location selected for new item + assert not nwTree.newTreeItem(nwItemType.FILE, None) + assert not nwTree.newTreeItem(nwItemType.FOLDER, None) + + # Select a location + chItem = nwTree._getTreeItem("a6d311a93600a") + nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) + chItem.setExpanded(True) + + # Create new item with no class set + assert nwTree.newTreeItem(nwItemType.FILE, None) + assert nwTree.newTreeItem(nwItemType.FOLDER, None) + + # Add roots + assert not nwTree.newTreeItem(nwItemType.ROOT, None) # Defaults to NOVEL + assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid + + # Check that we have the correct tree order + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "73475cb40a568", "44cb730c42048" + ] + + # Move second item up twice (should give same result) + nwTree.setSelectedHandle("8c659a11cd429") + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048" + ] + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048" + ] + + # Move it back down four times (last to should be the same) + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "73475cb40a568", "44cb730c42048" + ] + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "8c659a11cd429", "44cb730c42048" + ] + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429" + ] + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "73475cb40a568", "44cb730c42048", "8c659a11cd429" + ] + + # Move a root item (top level items are different) twice + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 9 + nwTree.setSelectedHandle("9d5247ab588e0") + + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10 + + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10 + + # Add some content to the new file + nwGUI.openDocument("73475cb40a568") + nwGUI.docEditor.setText("# Hello World\n") + nwGUI.saveDocument() + assert path.isfile(path.join(nwMinimal, "content", "73475cb40a568.nwd")) + + # Delete the items we added earlier + nwTree.clearSelection() + assert not nwTree.emptyTrash() # No folder yet + assert not nwTree.deleteItem(None) + assert not nwTree.deleteItem("1111111111111") + assert nwTree.deleteItem("73475cb40a568") # New File + assert nwTree.deleteItem("44cb730c42048") # New Folder + assert nwTree.deleteItem("71ee45a3c0db9") # Custom Root + assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder + assert "44cb730c42048" not in nwGUI.theProject.projTree._treeOrder + assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder + + # The file is in trash, empty it + assert path.isfile(path.join(nwMinimal, "content", "73475cb40a568.nwd")) + assert nwTree.emptyTrash() + assert not nwTree.emptyTrash() # Already empty + assert not path.isfile(path.join(nwMinimal, "content", "73475cb40a568.nwd")) + assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder + + # Close the project + nwGUI.closeProject() + + # Add an orphaned file + orphFile = path.join(nwMinimal, "content", "1234567890abc.nwd") + with open(orphFile, mode="w+", encoding="utf8") as outFile: + outFile.write("# Hello World\n") + + # Open the project again + nwGUI.openProject(nwMinimal) + + # Check that the orphaned file was found and added to the tree + assert nwTree.orphRoot is not None + nwTree.flushTreeOrder() + assert "1234567890abc" not in nwGUI.theProject.projTree._treeOrder + orItem = nwTree._getTreeItem("1234567890abc") + assert orItem.text(nwTree.C_NAME) == "Orphaned File 1" + + # Move it to the Plot folder + # plItem = nwTree._getTreeItem("7695ce551d265") + # orRect = nwTree.visualItemRect(orItem) + # plRect = nwTree.visualItemRect(plItem) + + # qtbot.mouseMove(nwTree.viewport(), pos=orRect.center(), delay=1000) + # qtbot.mousePress(nwTree.viewport(), Qt.LeftButton, pos=orRect.center(), delay=1000) + # qtbot.mouseMove(nwTree.viewport(), pos=plRect.center(), delay=1000) + # qtbot.mouseRelease(nwTree.viewport(), Qt.LeftButton, pos=plRect.center(), delay=1000) + + # qtbot.stopForInteraction() + nwGUI.closeMain() + nwGUI.close() + @pytest.mark.gui def testEditFormatMenu(qtbot, nwLipsum, nwTemp): From fc096ddb0bb60d9fe19f0eb2b6f64a00044592a9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Sep 2020 18:17:14 +0200 Subject: [PATCH 26/51] Significant improvements to dialog tests --- nw/__init__.py | 2 +- nw/config.py | 2 +- nw/core/project.py | 13 ++-- nw/error.py | 2 +- nw/gui/build.py | 4 +- nw/gui/docsplit.py | 2 +- nw/gui/mainmenu.py | 8 +-- nw/gui/preferences.py | 4 +- nw/gui/projload.py | 4 +- nw/gui/projsettings.py | 16 ++++- nw/gui/projtree.py | 8 +-- nw/gui/writingstats.py | 4 +- nw/guimain.py | 138 ++++++++++++++++++++++------------------- tests/conftest.py | 84 +++++++++++++++++++++++-- tests/test_dialogs.py | 100 ++++++++++++++++++++++++----- tests/test_gui.py | 4 ++ tests/test_project.py | 4 +- 17 files changed, 286 insertions(+), 113 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 95f15281..e9432711 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -195,7 +195,7 @@ def main(sysArgs=None): testMode = True # Set Config Options - CONFIG.blockGUI = not testMode + CONFIG.showGUI = not testMode CONFIG.debugInfo = debugLevel < logging.INFO CONFIG.cmdOpen = cmdOpen diff --git a/nw/config.py b/nw/config.py index 93141fa5..db8a6c17 100644 --- a/nw/config.py +++ b/nw/config.py @@ -56,7 +56,7 @@ class Config: self.appHandle = self.appName.lower() # Debug Settings - self.blockGUI = True # Allow blocking the GUI (disabled for testing) + self.showGUI = True # Allow blocking the GUI (disabled for testing) self.debugInfo = False # True if log level is DEBUG or VERBOSE # Config Error Handling diff --git a/nw/core/project.py b/nw/core/project.py index 5efce6ad..f7c18bdd 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -382,6 +382,7 @@ class NWProject(): # ========================== if not self.ensureFolderStructure(): + self.clearProject() return False self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) @@ -475,6 +476,7 @@ class NWProject(): "Project file does not appear to be a novelWriterXML file.", nwAlert.ERROR ) + self.clearProject() return False # Check Project Storage Version @@ -489,7 +491,7 @@ class NWProject(): # parser will lose the autoReplace settings if allowed to # read the file. Introduced in version 0.10. - if fileVersion == "1.0" and self.mainConf.blockGUI: + if fileVersion == "1.0" and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( "The project file and data is created by a novelWriter version " @@ -499,9 +501,10 @@ class NWProject(): "any more, so make sure you have a recent backup." )) if msgRes != QMessageBox.Yes: + self.clearProject() return False - elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.blockGUI: + elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI: self.makeAlert(( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " @@ -509,12 +512,13 @@ class NWProject(): ).format( vers = appVersion, ), nwAlert.ERROR) + self.clearProject() return False # Check novelWriter Version # ========================= - if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.blockGUI: + if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( "This project was saved by a newer version of novelWriter, version %s. " @@ -525,6 +529,7 @@ class NWProject(): appVersion, nw.__version__ )) if msgRes != QMessageBox.Yes: + self.clearProject() return False # Start Parsing the XML @@ -927,7 +932,7 @@ class NWProject(): return False if path.isdir(projPath): - if self.mainConf.blockGUI and listdir(self.projPath): + if self.mainConf.showGUI and listdir(self.projPath): self.theParent.makeAlert(( "New project folder is not empty. " "Each project requires a dedicated project folder." diff --git a/nw/error.py b/nw/error.py index fee8fab6..a8f3c149 100644 --- a/nw/error.py +++ b/nw/error.py @@ -160,7 +160,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False): errMsg = NWErrorMessage(nwGUI) errMsg.setMessage(exType, exValue, exTrace) - if nw.CONFIG.blockGUI: + if nw.CONFIG.showGUI: errMsg.exec_() try: diff --git a/nw/gui/build.py b/nw/gui/build.py index 52b32ddb..f2c15221 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -634,7 +634,7 @@ class GuiBuildNovel(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog saveTo = QFileDialog.getSaveFileName( @@ -746,7 +746,7 @@ class GuiBuildNovel(QDialog): errMsg = "Unknown format" # Report to user - if self.mainConf.blockGUI: + if self.mainConf.showGUI: if wSuccess: self.theParent.makeAlert( "%s file successfully written to:
%s" % ( diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 766d84f5..786ac8fa 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -163,7 +163,7 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Split Document", ( diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 5dbbede3..6e8e8cf7 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -192,7 +192,7 @@ class GuiMainMenu(QMenuBar): self.aOpenProject = QAction("Open Project", self) self.aOpenProject.setStatusTip("Open project") self.aOpenProject.setShortcut("Ctrl+Shift+O") - self.aOpenProject.triggered.connect(lambda: self.theParent.manageProjects()) + self.aOpenProject.triggered.connect(lambda: self.theParent.showProjectLoadDialog()) self.projMenu.addAction(self.aOpenProject) # Project > Save Project @@ -213,7 +213,7 @@ class GuiMainMenu(QMenuBar): self.aProjectSettings = QAction("Project Settings", self) self.aProjectSettings.setStatusTip("Project settings") self.aProjectSettings.setShortcut("Ctrl+Shift+,") - self.aProjectSettings.triggered.connect(lambda: self.theParent.editProjectDialog()) + self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) self.projMenu.addAction(self.aProjectSettings) # Project > Separator @@ -829,7 +829,7 @@ class GuiMainMenu(QMenuBar): self.aBuildProject = QAction("Build Novel Project", self) self.aBuildProject.setStatusTip("Launch the Build novel project tool") self.aBuildProject.setShortcut("F5") - self.aBuildProject.triggered.connect(lambda: self.theParent.buildProjectDialog()) + self.aBuildProject.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) self.toolsMenu.addAction(self.aBuildProject) # Tools > Writing Stats @@ -843,7 +843,7 @@ class GuiMainMenu(QMenuBar): self.aPreferences = QAction("Preferences", self) self.aPreferences.setStatusTip("Preferences") self.aPreferences.setShortcut("Ctrl+,") - self.aPreferences.triggered.connect(lambda: self.theParent.editConfigDialog()) + self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) self.toolsMenu.addAction(self.aPreferences) return diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 121d2fd1..5cbfbc72 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -104,7 +104,7 @@ class GuiPreferences(PagedDialog): validEntries &= retA needsRestart |= retB - if needsRestart and self.mainConf.blockGUI: + if needsRestart: msgBox = QMessageBox() msgBox.information( self, "Preferences", @@ -120,7 +120,7 @@ class GuiPreferences(PagedDialog): """Close the preferences without saving the changes. """ logger.verbose("ConfigEditor close button clicked") - self.close() + self.reject() return # END Class GuiPreferences diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 54e40a11..804a4c2e 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -178,7 +178,7 @@ class GuiProjectLoad(QDialog): """Browse for a folder path. """ logger.verbose("GuiProjectLoad browse button clicked") - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog projFile, _ = QFileDialog.getOpenFileName( @@ -219,7 +219,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: doRemove = False - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Remove Entry", diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index f5afa35e..998ec83c 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -124,13 +124,25 @@ class GuiProjectSettings(PagedDialog): newList = self.tabReplace.getNewList() self.theProject.setAutoReplace(newList) - self._doClose() + self._saveGuiSettings() + self.accept() return def _doClose(self): """Save settings and close the dialog. """ + self._saveGuiSettings() + self.reject() + return + + ## + # Internal Functions + ## + + def _saveGuiSettings(self): + """Save GUI settings. + """ winWidth = self.mainConf.rpxInt(self.width()) winHeight = self.mainConf.rpxInt(self.height()) replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0)) @@ -139,8 +151,6 @@ class GuiProjectSettings(PagedDialog): self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) - self.close() - return # END Class GuiProjectSettings diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index f4280f30..b5f8443b 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -261,7 +261,7 @@ class GuiProjectTree(QTreeWidget): """Move an item up or down in the tree, but only if the treeView has focus. This also applies when the menu is used. """ - hasFocus = qApp.focusWidget() == self or not self.mainConf.blockGUI + hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI if hasFocus and self.theParent.hasProject: tHandle = self.getSelectedHandle() @@ -362,7 +362,7 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The Trash folder is already empty.", nwAlert.INFO) return False - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( self, "Empty Trash", "Permanently delete %d file%s from Trash?" % ( @@ -416,7 +416,7 @@ class GuiProjectTree(QTreeWidget): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False - if self.mainConf.blockGUI and not alreadyAsked: + if self.mainConf.showGUI and not alreadyAsked: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName @@ -445,7 +445,7 @@ class GuiProjectTree(QTreeWidget): # The file is not already in the trash folder, so we # move it there. doTrash = False - if self.mainConf.blockGUI and askForTrash: + if self.mainConf.showGUI and askForTrash: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index c356f7f4..d4a4e5d6 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -326,7 +326,7 @@ class GuiWritingStats(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog saveTo = QFileDialog.getSaveFileName( @@ -380,7 +380,7 @@ class GuiWritingStats(QDialog): errMsg = str(e) # Report to user - if self.mainConf.blockGUI: + if self.mainConf.showGUI: if wSuccess: self.theParent.makeAlert( "%s file successfully written to:
%s" % ( diff --git a/nw/guimain.py b/nw/guimain.py index 06828f2e..483f095d 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -195,8 +195,8 @@ class GuiMain(QMainWindow): self.setStatus = self.statusBar.setStatus self.setProjectStatus = self.statusBar.setProjectStatus - if self.mainConf.blockGUI: - self.show() + # Force a show of the GUI + self.show() # Check that config loaded fine self.reportConfErr() @@ -218,7 +218,8 @@ class GuiMain(QMainWindow): logger.debug("Opening project from additional command line option") self.openProject(self.mainConf.cmdOpen) else: - self.manageProjects() + if self.mainConf.showGUI: + self.showProjectLoadDialog() logger.debug("novelWriter is ready ...") self.statusBar.setStatus("novelWriter is ready ...") @@ -245,24 +246,6 @@ class GuiMain(QMainWindow): # Project Actions ## - def manageProjects(self): - """Opens the projects dialog for selecting either existing - projects from a cache of recently opened projects, or provide a - browse button for projects not yet cached. - """ - if not self.mainConf.blockGUI: - return False - - dlgProj = GuiProjectLoad(self) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: - if dlgProj.openState == GuiProjectLoad.OPEN_STATE: - self.openProject(dlgProj.openPath) - elif dlgProj.openState == GuiProjectLoad.NEW_STATE: - self.newProject() - - return True - def newProject(self, projData=None, forceNew=False): """Create new project with a few default files and folders. The variable forceNew is used for testing. @@ -275,8 +258,8 @@ class GuiMain(QMainWindow): ) return False - if projData is None and self.mainConf.blockGUI: - projData = self.newProjectDialog() + if projData is None and self.mainConf.showGUI: + projData = self.showNewProjectDialog() if projData is None: return False @@ -316,7 +299,7 @@ class GuiMain(QMainWindow): # There is no project loaded, everything OK return True - if self.mainConf.blockGUI and not isYes: + if self.mainConf.showGUI and not isYes: msgBox = QMessageBox() msgRes = msgBox.question( self, "Close Project", "Save changes and close current project?" @@ -332,7 +315,7 @@ class GuiMain(QMainWindow): doBackup = False if self.theProject.doBackup and self.mainConf.backupOnClose: doBackup = True - if self.mainConf.blockGUI and self.mainConf.askBeforeBackup: + if self.mainConf.showGUI and self.mainConf.askBeforeBackup: msgBox = QMessageBox() msgRes = msgBox.question( self, "Backup Project", "Backup current project?" @@ -379,7 +362,7 @@ class GuiMain(QMainWindow): # reason handled by the project class. return False - if self.mainConf.blockGUI: + if self.mainConf.showGUI: try: lockDetails = ( "

The project was locked by the computer " @@ -449,7 +432,7 @@ class GuiMain(QMainWindow): # If the project is new, it may not have a path, so we need one if self.theProject.projPath is None: - projPath = self.saveProjectDialog() + projPath = self.selectProjectPath() self.theProject.setProjectPath(projPath) if self.theProject.projPath is None: return False @@ -611,7 +594,7 @@ class GuiMain(QMainWindow): return False if not self.docEditor.isEmpty(): - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self, "Import Document", ( "Importing the file will overwrite the current content of the document. " @@ -629,7 +612,7 @@ class GuiMain(QMainWindow): def mergeDocuments(self): """Merge multiple documents to one single new document. """ - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgMerge = GuiDocMerge(self, self.theProject) dlgMerge.exec_() return True @@ -637,7 +620,7 @@ class GuiMain(QMainWindow): def splitDocument(self): """Split a single document into multiple documents. """ - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgSplit = GuiDocSplit(self, self.theProject) dlgSplit.exec_() return True @@ -684,7 +667,7 @@ class GuiMain(QMainWindow): return logger.verbose("Requesting change to item %s" % tHandle) - if self.mainConf.blockGUI: + if self.mainConf.showGUI: dlgProj = GuiItemEditor(self, self.theProject, tHandle) dlgProj.exec_() if dlgProj.result() == QDialog.Accepted: @@ -746,7 +729,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() - if self.mainConf.blockGUI and not beQuiet: + if self.mainConf.showGUI and not beQuiet: self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO) return True @@ -763,7 +746,7 @@ class GuiMain(QMainWindow): # Main Dialogs ## - def saveProjectDialog(self): + def selectProjectPath(self): """Select where to save project. """ dlgOpt = QFileDialog.Options() @@ -776,7 +759,22 @@ class GuiMain(QMainWindow): return projPath return None - def newProjectDialog(self): + def showProjectLoadDialog(self): + """Opens the projects dialog for selecting either existing + projects from a cache of recently opened projects, or provide a + browse button for projects not yet cached. + """ + dlgProj = GuiProjectLoad(self) + dlgProj.exec_() + if dlgProj.result() == QDialog.Accepted: + if dlgProj.openState == GuiProjectLoad.OPEN_STATE: + self.openProject(dlgProj.openPath) + elif dlgProj.openState == GuiProjectLoad.NEW_STATE: + self.newProject() + + return True + + def showNewProjectDialog(self): """Open the wizard and assemble the project options dict. """ newProj = GuiProjectWizard(self) @@ -787,62 +785,76 @@ class GuiMain(QMainWindow): return None - def editConfigDialog(self): + def showPreferencesDialog(self): """Open the preferences dialog. """ dlgConf = GuiPreferences(self, self.theProject) - if dlgConf.exec_() == QDialog.Accepted: + dlgConf.exec_() + + if dlgConf.result() == QDialog.Accepted: logger.debug("Applying new preferences") self.initMain() self.theTheme.updateTheme() self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() - return True - def editProjectDialog(self): + return + + def showProjectSettingsDialog(self): """Open the project settings dialog. """ - if self.hasProject: - dlgProj = GuiProjectSettings(self, self.theProject) - dlgProj.exec_() + if not self.hasProject: + logger.error("No project open") + return + + dlgProj = GuiProjectSettings(self, self.theProject) + dlgProj.exec_() + + if dlgProj.result() == QDialog.Accepted: + logger.debug("Applying new project settings") self.docEditor.setDictionaries() self._setWindowTitle(self.theProject.projName) - return True - def buildProjectDialog(self): + return + + def showBuildProjectDialog(self): """Open the build project dialog. """ - if self.hasProject: - dlgBuild = GuiBuildNovel(self, self.theProject) - dlgBuild.setModal(False) - dlgBuild.show() - return True + if not self.hasProject: + logger.error("No project open") + return + + dlgBuild = GuiBuildNovel(self, self.theProject) + dlgBuild.setModal(False) + dlgBuild.show() + return def showWritingStatsDialog(self): """Open the session log dialog. """ - if self.hasProject: - dlgStats = GuiWritingStats(self, self.theProject) - dlgStats.setModal(False) - dlgStats.show() - return True + if not self.hasProject: + logger.error("No project open") + return + + dlgStats = GuiWritingStats(self, self.theProject) + dlgStats.setModal(False) + dlgStats.show() + return def showAboutNWDialog(self): """Show the about dialog for novelWriter. """ - if self.mainConf.blockGUI: - dlgAbout = GuiAbout(self) - dlgAbout.exec_() - return True + dlgAbout = GuiAbout(self) + dlgAbout.exec_() + return def showAboutQtDialog(self): """Show the about dialog for Qt. """ - if self.mainConf.blockGUI: - msgBox = QMessageBox() - msgBox.aboutQt(self, "About Qt") - return True + msgBox = QMessageBox() + msgBox.aboutQt(self, "About Qt") + return def makeAlert(self, theMessage, theLevel=nwAlert.INFO): """Alert both the user and the logger at the same time. Message @@ -871,7 +883,7 @@ class GuiMain(QMainWindow): logger.error(msgLine) # Popup - if self.mainConf.blockGUI: + if self.mainConf.showGUI: msgBox = QMessageBox() if theLevel == nwAlert.INFO: msgBox.information(self, "Information", popMsg) @@ -902,7 +914,7 @@ class GuiMain(QMainWindow): def closeMain(self): """Save everything, and close novelWriter. """ - if self.mainConf.blockGUI and self.hasProject: + if self.mainConf.showGUI and self.hasProject: msgBox = QMessageBox() msgRes = msgBox.question( self, "Exit", "Do you want to save changes and exit?" diff --git a/tests/conftest.py b/tests/conftest.py index 14988efa..a8945155 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,12 +9,22 @@ import shutil from os import path, mkdir from nwdummy import DummyMain +from PyQt5.QtWidgets import QFileDialog, QMessageBox + sys.path.insert(1, path.abspath(path.join(path.dirname(__file__), path.pardir))) from nw.config import Config # noqa: E402 +## +# Core Test Folders +## + @pytest.fixture(scope="session") def nwTemp(): + """A temporary folder for the test session. This folder is + presistent after the test so that the status of generated files can + be checked. The folder is instead cleared before a new test session. + """ testDir = path.dirname(__file__) tempDir = path.join(testDir, "temp") if path.isdir(tempDir): @@ -25,31 +35,51 @@ def nwTemp(): @pytest.fixture(scope="session") def nwRef(): + """The folder where all the reference files are stored for verifying + the results of tests. + """ testDir = path.dirname(__file__) refDir = path.join(testDir, "reference") return refDir -@pytest.fixture(scope="session") -def nwConf(nwRef, nwTemp): - theConf = Config() - theConf.initConfig(nwRef, nwTemp) - return theConf +## +# novelWriter Objects +## @pytest.fixture(scope="session") def tmpConf(nwTemp): + """Create a temporary novelWriter configuration object. + """ theConf = Config() theConf.initConfig(nwTemp, nwTemp) theConf.setLastPath("") return theConf +@pytest.fixture(scope="session") +def nwConf(nwRef, nwTemp): + """Temporary novelWriter configuration used for the dummy instance + of novelWriter's main GUI. + """ + theConf = Config() + theConf.initConfig(nwRef, nwTemp) + return theConf + @pytest.fixture(scope="session") def nwDummy(nwRef, nwTemp, nwConf): + """Create a dummy instance of novelWriter's main GUI class. + """ theDummy = DummyMain() theDummy.mainConf = nwConf return theDummy +## +# Temporary Test Folders +## + @pytest.fixture(scope="session") def nwTempProj(nwTemp): + """A temporary folder for project tests. + """ projDir = path.join(nwTemp, "proj") if not path.isdir(projDir): mkdir(projDir) @@ -57,6 +87,8 @@ def nwTempProj(nwTemp): @pytest.fixture(scope="session") def nwTempGUI(nwTemp): + """A temporary folder for GUI tests. + """ guiDir = path.join(nwTemp, "gui") if not path.isdir(guiDir): mkdir(guiDir) @@ -64,6 +96,8 @@ def nwTempGUI(nwTemp): @pytest.fixture(scope="session") def nwTempBuild(nwTemp): + """A temporary folder for build tests. + """ buildDir = path.join(nwTemp, "build") if not path.isdir(buildDir): mkdir(buildDir) @@ -71,6 +105,8 @@ def nwTempBuild(nwTemp): @pytest.fixture(scope="function") def nwFuncTemp(nwTemp): + """A temporary folder for a single test function. + """ funcDir = path.join(nwTemp, "ftemp") if path.isdir(funcDir): shutil.rmtree(funcDir) @@ -81,8 +117,14 @@ def nwFuncTemp(nwTemp): shutil.rmtree(funcDir) return +## +# Temp Folders for Projects +## + @pytest.fixture(scope="function") def nwMinimal(nwTemp): + """A minimal novelWriter example project. + """ testDir = path.dirname(__file__) minimalStore = path.join(testDir, "minimal") minimalDir = path.join(nwTemp, "minimal") @@ -102,6 +144,9 @@ def nwMinimal(nwTemp): @pytest.fixture(scope="function") def nwLipsum(nwTemp): + """A medium sized novelWriter example project with a lot of Lorem + Ipsum dummy text. + """ testDir = path.dirname(__file__) lipsumStore = path.join(testDir, "lipsum") lipsumDir = path.join(nwTemp, "lipsum") @@ -121,6 +166,8 @@ def nwLipsum(nwTemp): @pytest.fixture(scope="function") def nwOldProj(nwTemp): + """A minimal movelWriter project using the old folder structure. + """ testDir = path.dirname(__file__) oldProjStore = path.join(testDir, "oldproj") oldProjDir = path.join(nwTemp, "oldproj") @@ -131,3 +178,30 @@ def nwOldProj(nwTemp): if path.isdir(oldProjDir): shutil.rmtree(oldProjDir) return + +## +# Monkey Patch Dialogs +## + +@pytest.fixture(scope="function") +def mnkQtDialogs(monkeypatch, nwTemp): + """Mock Qt dialog functions to prevent GUI blocking while testing. + """ + monkeypatch.setattr( + QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp + ) + + monkeypatch.setattr( + QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes + ) + monkeypatch.setattr( + QMessageBox, "information", lambda *args, **kwargs: None + ) + monkeypatch.setattr( + QMessageBox, "warning", lambda *args, **kwargs: None + ) + monkeypatch.setattr( + QMessageBox, "critical", lambda *args, **kwargs: None + ) + + return diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 1fceaca1..13d41afb 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -7,13 +7,14 @@ import pytest import json from shutil import copyfile -from nwtools import cmpFiles +from nwtools import cmpFiles, getGuiItem from os import path from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtWidgets import ( - QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog + QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog, QAction, + QMessageBox ) from nw.gui import ( @@ -28,22 +29,34 @@ keyDelay = 2 stepDelay = 20 @pytest.mark.gui -def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testProjectSettings(qtbot, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - # Create new, save, open project + # Check that we cannot open when there is no project + nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) + assert getGuiItem("GuiProjectSettings") is None + + # Create new project nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": nwFuncTemp}, True) nwGUI.mainConf.backupPath = nwFuncTemp - projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject) + # Get the dialog object + monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None) + monkeypatch.setattr(GuiProjectSettings, "result", lambda *args: QDialog.Accepted) + nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000) + + projEdit = getGuiItem("GuiProjectSettings") + assert isinstance(projEdit, GuiProjectSettings) projEdit.show() qtbot.addWidget(projEdit) + # Main settings qtbot.wait(stepDelay) projEdit.tabMain.editName.setText("") for c in "Project Name": @@ -199,6 +212,10 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): assert nwGUI.closeProject() qtbot.wait(stepDelay) + # Check that we cannot open when there is no project + nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) + assert getGuiItem("GuiWritingStats") is None + assert nwGUI.openProject(nwFuncTemp) qtbot.wait(stepDelay) @@ -239,8 +256,11 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): qtbot.wait(stepDelay) nwGUI.mainConf.lastPath = nwFuncTemp - sessLog = GuiWritingStats(nwGUI, nwGUI.theProject) - sessLog.show() + nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000) + + sessLog = getGuiItem("GuiWritingStats") + assert isinstance(sessLog, GuiWritingStats) qtbot.wait(stepDelay) assert sessLog._saveData(sessLog.FMT_CSV) @@ -337,17 +357,29 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testAboutBox(qtbot, nwFuncTemp, nwTemp): +def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - msgAbout = GuiAbout(nwGUI) + # NW About + monkeypatch.setattr(GuiAbout, "exec_", lambda *args: None) + nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) + + msgAbout = getGuiItem("GuiAbout") + assert isinstance(msgAbout, GuiAbout) + msgAbout.show() + assert msgAbout.pageAbout.document().characterCount() > 100 assert msgAbout.pageLicense.document().characterCount() > 100 + # Qt About + monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None) + nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger) + # qtbot.stopForInteraction() msgAbout._doClose() nwGUI.closeMain() @@ -361,11 +393,20 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp): qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - assert nwGUI.openProject(nwLipsum) + # Check that we cannot open when there is no project + nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) + assert getGuiItem("GuiBuildNovel") is None + # Open a project + assert nwGUI.openProject(nwLipsum) nwGUI.mainConf.lastPath = nwLipsum - nwBuild = GuiBuildNovel(nwGUI, nwGUI.theProject) + # Open the tool + nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiBuildNovel") is not None, timeout=1000) + + nwBuild = getGuiItem("GuiBuildNovel") + assert isinstance(nwBuild, GuiBuildNovel) # Default Settings qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) @@ -764,7 +805,7 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testLoadProject(qtbot, nwMinimal, nwTemp): +def testLoadProject(qtbot, monkeypatch, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -774,7 +815,13 @@ def testLoadProject(qtbot, nwMinimal, nwTemp): assert nwGUI.openProject(nwMinimal) assert nwGUI.closeProject() - nwLoad = GuiProjectLoad(nwGUI) + monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *args: None) + monkeypatch.setattr(GuiProjectLoad, "result", lambda *args: QDialog.Accepted) + nwGUI.mainMenu.aOpenProject.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiProjectLoad") is not None, timeout=1000) + + nwLoad = getGuiItem("GuiProjectLoad") + assert isinstance(nwLoad, GuiProjectLoad) nwLoad.show() recentCount = nwLoad.listBox.topLevelItemCount() @@ -793,6 +840,7 @@ def testLoadProject(qtbot, nwMinimal, nwTemp): assert nwLoad.openPath == selPath assert nwLoad.openState == nwLoad.OPEN_STATE + # Just create a new project load from scratch for the rest of the test del nwLoad nwLoad = GuiProjectLoad(nwGUI) nwLoad.show() @@ -815,7 +863,7 @@ def testLoadProject(qtbot, nwMinimal, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): +def testPreferences(qtbot, monkeypatch, mnkQtDialogs, nwMinimal, nwTemp, nwRef, tmpConf): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -823,11 +871,17 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf): qtbot.wait(stepDelay) assert nwGUI.openProject(nwMinimal) - nwPrefs = GuiPreferences(nwGUI, nwGUI.theProject) + + monkeypatch.setattr(GuiPreferences, "exec_", lambda *args: None) + monkeypatch.setattr(GuiPreferences, "result", lambda *args: QDialog.Accepted) + nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) + + nwPrefs = getGuiItem("GuiPreferences") + assert isinstance(nwPrefs, GuiPreferences) nwPrefs.show() # Override Config - tmpConf.blockGUI = False tmpConf.confPath = nwMinimal nwGUI.mainConf = tmpConf nwPrefs.mainConf = tmpConf @@ -991,3 +1045,17 @@ def testQuotesDialog(qtbot, nwMinimal, nwTemp): nwQuot.close() nwGUI.closeMain() nwGUI.close() + +@pytest.mark.gui +def testDialogsOpenClose(qtbot, mnkQtDialogs, nwMinimal, nwTemp): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + assert nwGUI.selectProjectPath() == nwTemp + + # qtbot.stopForInteraction() + nwGUI.closeMain() + nwGUI.close() diff --git a/tests/test_gui.py b/tests/test_gui.py index ec24d5ec..8cf3a4d4 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -223,6 +223,10 @@ def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + # Trigger autosaves before making more changes + nwGUI._autoSaveDocument() + nwGUI._autoSaveProject() + # Select the 'New Scene' file nwGUI.setFocus(1) nwGUI.treeView.clearSelection() diff --git a/tests/test_project.py b/tests/test_project.py index 1ec42348..7e46b8e6 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -580,9 +580,9 @@ def testOrphanedFiles(nwDummy, nwLipsum): assert theProject.closeProject() @pytest.mark.project -def testOldProject(nwDummy, nwOldProj): +def testOldProject(nwDummy, nwOldProj, mnkQtDialogs): theProject = NWProject(nwDummy) - theProject.mainConf.blockGUI = False + theProject.mainConf.showGUI = False # Create dummy files for known legacy files deleteFiles = [ From 678a31855f56c01d0a86d67ea022950877244100 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Sep 2020 20:29:52 +0200 Subject: [PATCH 27/51] Improved coverage of dialogs tools, and import document feature --- nw/guimain.py | 29 ++++++++++++----------------- tests/conftest.py | 27 --------------------------- tests/test_dialogs.py | 30 ++++++++++++++++++++++++------ tests/test_gui.py | 42 ++++++++++++++++++++++++++++++++++++++++-- tests/test_project.py | 2 +- 5 files changed, 77 insertions(+), 53 deletions(-) diff --git a/nw/guimain.py b/nw/guimain.py index 483f095d..bc8d5e88 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -594,15 +594,12 @@ class GuiMain(QMainWindow): return False if not self.docEditor.isEmpty(): - if self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question(self, "Import Document", ( - "Importing the file will overwrite the current content of the document. " - "Do you want to proceed?" - )) - if msgRes != QMessageBox.Yes: - return False - else: + msgBox = QMessageBox() + msgRes = msgBox.question(self, "Import Document", ( + "Importing the file will overwrite the current content of the document. " + "Do you want to proceed?" + )) + if msgRes != QMessageBox.Yes: return False self.docEditor.replaceText(theText) @@ -612,18 +609,16 @@ class GuiMain(QMainWindow): def mergeDocuments(self): """Merge multiple documents to one single new document. """ - if self.mainConf.showGUI: - dlgMerge = GuiDocMerge(self, self.theProject) - dlgMerge.exec_() - return True + dlgMerge = GuiDocMerge(self, self.theProject) + dlgMerge.exec_() + return def splitDocument(self): """Split a single document into multiple documents. """ - if self.mainConf.showGUI: - dlgSplit = GuiDocSplit(self, self.theProject) - dlgSplit.exec_() - return True + dlgSplit = GuiDocSplit(self, self.theProject) + dlgSplit.exec_() + return def passDocumentAction(self, theAction): """Pass on document action theAction to the document viewer if diff --git a/tests/conftest.py b/tests/conftest.py index a8945155..b7ef709e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,30 +178,3 @@ def nwOldProj(nwTemp): if path.isdir(oldProjDir): shutil.rmtree(oldProjDir) return - -## -# Monkey Patch Dialogs -## - -@pytest.fixture(scope="function") -def mnkQtDialogs(monkeypatch, nwTemp): - """Mock Qt dialog functions to prevent GUI blocking while testing. - """ - monkeypatch.setattr( - QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp - ) - - monkeypatch.setattr( - QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes - ) - monkeypatch.setattr( - QMessageBox, "information", lambda *args, **kwargs: None - ) - monkeypatch.setattr( - QMessageBox, "warning", lambda *args, **kwargs: None - ) - monkeypatch.setattr( - QMessageBox, "critical", lambda *args, **kwargs: None - ) - - return diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 13d41afb..717065e6 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -14,7 +14,7 @@ from os import path from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtWidgets import ( QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog, QAction, - QMessageBox + QMessageBox, QFileDialog ) from nw.gui import ( @@ -547,7 +547,7 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): +def testMergeSplitTools(qtbot, monkeypatch, nwTempGUI, nwLipsum, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -562,7 +562,13 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): assert nwGUI.treeView.setSelectedHandle("45e6b01ca35c1") qtbot.wait(stepDelay) - nwMerge = GuiDocMerge(nwGUI, nwGUI.theProject) + monkeypatch.setattr(GuiDocMerge, "exec_", lambda *args: None) + nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiDocMerge") is not None, timeout=1000) + + nwMerge = getGuiItem("GuiDocMerge") + assert isinstance(nwMerge, GuiDocMerge) + nwMerge.show() qtbot.wait(stepDelay) nwMerge._doMerge() @@ -579,8 +585,16 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): # Split By Chapter assert nwGUI.treeView.setSelectedHandle("73475cb40a568") qtbot.wait(stepDelay) - nwSplit = GuiDocSplit(nwGUI, nwGUI.theProject) + + monkeypatch.setattr(GuiDocSplit, "exec_", lambda *args: None) + nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiDocSplit") is not None, timeout=1000) + + nwSplit = getGuiItem("GuiDocSplit") + assert isinstance(nwSplit, GuiDocSplit) + nwSplit.show() qtbot.wait(stepDelay) + nwSplit.splitLevel.setCurrentIndex(1) qtbot.wait(stepDelay) @@ -863,7 +877,7 @@ def testLoadProject(qtbot, monkeypatch, nwMinimal, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testPreferences(qtbot, monkeypatch, mnkQtDialogs, nwMinimal, nwTemp, nwRef, tmpConf): +def testPreferences(qtbot, monkeypatch, nwMinimal, nwTemp, nwRef, tmpConf): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -872,6 +886,8 @@ def testPreferences(qtbot, monkeypatch, mnkQtDialogs, nwMinimal, nwTemp, nwRef, assert nwGUI.openProject(nwMinimal) + monkeypatch.setattr(QMessageBox, "information", lambda *args, **kwargs: None) + monkeypatch.setattr(GuiPreferences, "exec_", lambda *args: None) monkeypatch.setattr(GuiPreferences, "result", lambda *args: QDialog.Accepted) nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) @@ -883,6 +899,7 @@ def testPreferences(qtbot, monkeypatch, mnkQtDialogs, nwMinimal, nwTemp, nwRef, # Override Config tmpConf.confPath = nwMinimal + tmpConf.showGUI = False nwGUI.mainConf = tmpConf nwPrefs.mainConf = tmpConf nwPrefs.tabGeneral.mainConf = tmpConf @@ -1047,13 +1064,14 @@ def testQuotesDialog(qtbot, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testDialogsOpenClose(qtbot, mnkQtDialogs, nwMinimal, nwTemp): +def testDialogsOpenClose(qtbot, monkeypatch, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp) assert nwGUI.selectProjectPath() == nwTemp # qtbot.stopForInteraction() diff --git a/tests/test_gui.py b/tests/test_gui.py index 8cf3a4d4..016325b2 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -12,7 +12,9 @@ from nwtools import cmpFiles from os import path from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon -from PyQt5.QtWidgets import qApp, QAction, QTreeWidgetItem, QStyle +from PyQt5.QtWidgets import ( + qApp, QAction, QTreeWidgetItem, QStyle, QFileDialog, QMessageBox +) from nw.constants import ( nwItemType, nwItemClass, nwUnicode, nwOutline, nwDocAction, nwDocInsert @@ -999,7 +1001,7 @@ def testContextMenu(qtbot, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testInsertMenu(qtbot, nwFuncTemp, nwTemp): +def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -1085,6 +1087,42 @@ def testInsertMenu(qtbot, nwFuncTemp, nwTemp): assert nwGUI.docEditor.getText() == " " nwGUI.docEditor.clear() + # Insert text from file + nwGUI.closeDocument() + + # First, with no path + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: []) + assert not nwGUI.importDocument() + + # Then with a path, but an invalid one + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: [" "]) + assert not nwGUI.importDocument() + + # Then a valid path, but bot a file that exists + theFile = path.join(nwTemp, "import.txt") + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: [theFile]) + assert not nwGUI.importDocument() + + # Create the file and try again, but with no target document open + with open(theFile, mode="w+", encoding="utf8") as outFile: + outFile.write("Foo") + assert not nwGUI.importDocument() + + # Open the document from before, and add some text to it + nwGUI.openDocument("0e17daca5f3e1") + nwGUI.docEditor.setText("Bar") + assert nwGUI.docEditor.getText() == "Bar" + + # The document isn't empty, so the message box should pop + monkeypatch.setattr(QMessageBox, "question", lambda *args, **kwargs: QMessageBox.No) + assert not nwGUI.importDocument() + assert nwGUI.docEditor.getText() == "Bar" + + # Finally, accept the replaced text, this time we use the menu entry to trigger it + monkeypatch.setattr(QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes) + nwGUI.mainMenu.aImportFile.activate(QAction.Trigger) + assert nwGUI.docEditor.getText() == "Foo" + # qtbot.stopForInteraction() nwGUI.closeMain() nwGUI.close() diff --git a/tests/test_project.py b/tests/test_project.py index 7e46b8e6..9ee7324c 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -580,7 +580,7 @@ def testOrphanedFiles(nwDummy, nwLipsum): assert theProject.closeProject() @pytest.mark.project -def testOldProject(nwDummy, nwOldProj, mnkQtDialogs): +def testOldProject(nwDummy, nwOldProj): theProject = NWProject(nwDummy) theProject.mainConf.showGUI = False From 28ef1d12b012745ba3391f01d537bc6b61d6c969 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Sep 2020 22:22:29 +0200 Subject: [PATCH 28/51] All message box questions are now handled in test suite, so no need to check for showGUI --- nw/gui/docsplit.py | 21 +++++------ nw/gui/projload.py | 18 +++------ nw/gui/projtree.py | 19 +++++----- nw/gui/writingstats.py | 25 ++++++------- nw/guimain.py | 85 ++++++++++++++++++++---------------------- tests/conftest.py | 24 +++++++++++- tests/test_dialogs.py | 63 +++++++++++++++++++++++++------ tests/test_gui.py | 65 ++++++++++++++++++++++++++------ 8 files changed, 207 insertions(+), 113 deletions(-) diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 786ac8fa..836fd5cb 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -163,17 +163,16 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return - if self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question( - self, "Split Document", ( - "The document will be split into %d file(s) in a new folder. " - "The original document will remain intact.

" - "Continue with the splitting process?" - ) % nFiles - ) - if msgRes != QMessageBox.Yes: - return + msgBox = QMessageBox() + msgRes = msgBox.question( + self, "Split Document", ( + "The document will be split into %d file(s) in a new folder. " + "The original document will remain intact.

" + "Continue with the splitting process?" + ) % nFiles + ) + if msgRes != QMessageBox.Yes: + return # Create the folder fHandle = self.theProject.newFolder( diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 804a4c2e..c19175be 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -218,18 +218,12 @@ class GuiProjectLoad(QDialog): """ selList = self.listBox.selectedItems() if selList: - doRemove = False - if self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question( - self, "Remove Entry", - "Remove the selected entry from the recent projects list?" - ) - doRemove = (msgRes == QMessageBox.Yes) - else: - doRemove = True - - if doRemove: + msgBox = QMessageBox() + msgRes = msgBox.question( + self, "Remove Entry", + "Remove the selected entry from the recent projects list?" + ) + if msgRes == QMessageBox.Yes: self.mainConf.removeFromRecentCache( selList[0].data(self.C_NAME, Qt.UserRole) ) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index b5f8443b..00b81dac 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -362,15 +362,14 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The Trash folder is already empty.", nwAlert.INFO) return False - if self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question( - self, "Empty Trash", "Permanently delete %d file%s from Trash?" % ( - nTrash, "s" if nTrash > 1 else "" - ) + msgBox = QMessageBox() + msgRes = msgBox.question( + self, "Empty Trash", "Permanently delete %d file%s from Trash?" % ( + nTrash, "s" if nTrash > 1 else "" ) - if msgRes != QMessageBox.Yes: - return False + ) + if msgRes != QMessageBox.Yes: + return False logger.verbose("Deleting %d files from Trash" % nTrash) for tHandle in self.getTreeFromHandle(trashHandle): @@ -416,7 +415,7 @@ class GuiProjectTree(QTreeWidget): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False - if self.mainConf.showGUI and not alreadyAsked: + if not alreadyAsked: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName @@ -445,7 +444,7 @@ class GuiProjectTree(QTreeWidget): # The file is not already in the trash folder, so we # move it there. doTrash = False - if self.mainConf.showGUI and askForTrash: + if askForTrash: msgBox = QMessageBox() msgRes = msgBox.question( self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index d4a4e5d6..dd272b8b 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -380,19 +380,18 @@ class GuiWritingStats(QDialog): errMsg = str(e) # Report to user - if self.mainConf.showGUI: - if wSuccess: - self.theParent.makeAlert( - "%s file successfully written to:
%s" % ( - textFmt, savePath - ), nwAlert.INFO - ) - else: - self.theParent.makeAlert( - "Failed to write %s file. %s" % ( - textFmt, errMsg - ), nwAlert.ERROR - ) + if wSuccess: + self.theParent.makeAlert( + "%s file successfully written to:
%s" % ( + textFmt, savePath + ), nwAlert.INFO + ) + else: + self.theParent.makeAlert( + "Failed to write %s file. %s" % ( + textFmt, errMsg + ), nwAlert.ERROR + ) return True diff --git a/nw/guimain.py b/nw/guimain.py index bc8d5e88..859145df 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -251,14 +251,13 @@ class GuiMain(QMainWindow): The variable forceNew is used for testing. """ if self.hasProject: - msgBox = QMessageBox() - msgBox.warning( - self, "New Project", - "Please close the current project before making a new one." + self.makeAlert( + "Please close the current project before making a new one.", + nwAlert.ERROR ) return False - if projData is None and self.mainConf.showGUI: + if projData is None: projData = self.showNewProjectDialog() if projData is None: @@ -270,10 +269,9 @@ class GuiMain(QMainWindow): return False if path.isfile(path.join(projPath, self.theProject.projFile)) and not forceNew: - msgBox = QMessageBox() - msgBox.critical( - self, "New Project", - "A project already exists in that location. Please choose another folder." + self.makeAlert( + "A project already exists in that location. Please choose another folder.", + nwAlert.ERROR ) return False @@ -299,7 +297,7 @@ class GuiMain(QMainWindow): # There is no project loaded, everything OK return True - if self.mainConf.showGUI and not isYes: + if not isYes: msgBox = QMessageBox() msgRes = msgBox.question( self, "Close Project", "Save changes and close current project?" @@ -315,7 +313,7 @@ class GuiMain(QMainWindow): doBackup = False if self.theProject.doBackup and self.mainConf.backupOnClose: doBackup = True - if self.mainConf.showGUI and self.mainConf.askBeforeBackup: + if self.mainConf.askBeforeBackup: msgBox = QMessageBox() msgRes = msgBox.question( self, "Backup Project", "Backup current project?" @@ -362,39 +360,38 @@ class GuiMain(QMainWindow): # reason handled by the project class. return False - if self.mainConf.showGUI: - try: - lockDetails = ( - "

The project was locked by the computer " - "'%s' (%s %s), last active on %s" - ) % ( - self.theProject.lockedBy[0], - self.theProject.lockedBy[1], - self.theProject.lockedBy[2], - datetime.fromtimestamp( - int(self.theProject.lockedBy[3]) - ).strftime("%x %X") - ) - except Exception: - lockDetails = "" - - msgBox = QMessageBox() - msgRes = msgBox.warning( - self, "Project Locked", ( - "The project is already open by another instance of novelWriter, and " - "is therefore locked. Override lock and continue anyway?

" - "Note: If the program or the computer previously crashed, the lock " - "can safely be overridden. If, however, another instance of " - "novelWriter has the project open, overriding the lock may corrupt " - "the project, and is not recommended.%s" - ) % lockDetails, - QMessageBox.Yes | QMessageBox.No, QMessageBox.No + try: + lockDetails = ( + "

The project was locked by the computer " + "'%s' (%s %s), last active on %s" + ) % ( + self.theProject.lockedBy[0], + self.theProject.lockedBy[1], + self.theProject.lockedBy[2], + datetime.fromtimestamp( + int(self.theProject.lockedBy[3]) + ).strftime("%x %X") ) - if msgRes == QMessageBox.Yes: - if not self.theProject.openProject(projFile, overrideLock=True): - return False - else: + except Exception: + lockDetails = "" + + msgBox = QMessageBox() + msgRes = msgBox.warning( + self, "Project Locked", ( + "The project is already open by another instance of novelWriter, and " + "is therefore locked. Override lock and continue anyway?

" + "Note: If the program or the computer previously crashed, the lock " + "can safely be overridden. If, however, another instance of " + "novelWriter has the project open, overriding the lock may corrupt " + "the project, and is not recommended.%s" + ) % lockDetails, + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if msgRes == QMessageBox.Yes: + if not self.theProject.openProject(projFile, overrideLock=True): return False + else: + return False # Project is loaded self.hasProject = True @@ -724,7 +721,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() - if self.mainConf.showGUI and not beQuiet: + if not beQuiet: self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO) return True @@ -909,7 +906,7 @@ class GuiMain(QMainWindow): def closeMain(self): """Save everything, and close novelWriter. """ - if self.mainConf.showGUI and self.hasProject: + if self.hasProject: msgBox = QMessageBox() msgRes = msgBox.question( self, "Exit", "Do you want to save changes and exit?" diff --git a/tests/conftest.py b/tests/conftest.py index b7ef709e..bafa1fbb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ import shutil from os import path, mkdir from nwdummy import DummyMain -from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtWidgets import QMessageBox sys.path.insert(1, path.abspath(path.join(path.dirname(__file__), path.pardir))) @@ -178,3 +178,25 @@ def nwOldProj(nwTemp): if path.isdir(oldProjDir): shutil.rmtree(oldProjDir) return + +## +# Monkey Patch Dialogs +## + +@pytest.fixture(scope="function") +def yesToAll(monkeypatch): + """Make the message boxes/questions always say yes to the dress! + """ + monkeypatch.setattr( + QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes + ) + monkeypatch.setattr( + QMessageBox, "information", lambda *args, **kwargs: QMessageBox.Yes + ) + monkeypatch.setattr( + QMessageBox, "warning", lambda *args, **kwargs: QMessageBox.Yes + ) + monkeypatch.setattr( + QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes + ) + return diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 717065e6..464b39bc 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -29,7 +29,7 @@ keyDelay = 2 stepDelay = 20 @pytest.mark.gui -def testProjectSettings(qtbot, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -139,7 +139,7 @@ def testProjectSettings(qtbot, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp nwGUI.closeMain() @pytest.mark.gui -def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -198,7 +198,7 @@ def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # qtbot.stopForInteraction() @pytest.mark.gui -def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp): +def testWritingStatsExport(qtbot, yesToAll, nwFuncTemp, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -385,7 +385,7 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp): +def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -547,7 +547,7 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testMergeSplitTools(qtbot, monkeypatch, nwTempGUI, nwLipsum, nwRef, nwTemp): +def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -690,7 +690,7 @@ def testMergeSplitTools(qtbot, monkeypatch, nwTempGUI, nwLipsum, nwRef, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testNewProjectWizard(qtbot, nwLipsum, nwTemp): +def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): from PyQt5.QtWidgets import QWizard from nw.gui.projwizard import ( @@ -698,7 +698,47 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp): ProjWizardCustomPage, ProjWizardFinalPage ) - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + ## + # Test New Project Function + ## + + # New with a project open should cause an error + assert nwGUI.openProject(nwMinimal) + assert not nwGUI.newProject() + + # Close project, but call with invalid path + assert nwGUI.closeProject() + monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: None) + assert not nwGUI.newProject() + + # Now, with an empty dictionary + monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {}) + assert not nwGUI.newProject() + + # Now, with a non-empty folder + monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {"projPath": nwMinimal}) + assert not nwGUI.newProject() + + # Force overwrite + monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {"projPath": nwMinimal}) + assert nwGUI.newProject(forceNew=True) + + nwGUI.closeMain() + nwGUI.close() + + # qtbot.stopForInteraction() + + ## + # Test the Wizard + ## + + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -817,9 +857,10 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui -def testLoadProject(qtbot, monkeypatch, nwMinimal, nwTemp): +def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -877,7 +918,7 @@ def testLoadProject(qtbot, monkeypatch, nwMinimal, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testPreferences(qtbot, monkeypatch, nwMinimal, nwTemp, nwRef, tmpConf): +def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -1034,7 +1075,7 @@ def testPreferences(qtbot, monkeypatch, nwMinimal, nwTemp, nwRef, tmpConf): assert cmpFiles(testConf, refConf, ignoreLines) @pytest.mark.gui -def testQuotesDialog(qtbot, nwMinimal, nwTemp): +def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -1064,7 +1105,7 @@ def testQuotesDialog(qtbot, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testDialogsOpenClose(qtbot, monkeypatch, nwMinimal, nwTemp): +def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() diff --git a/tests/test_gui.py b/tests/test_gui.py index 016325b2..5289b187 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -88,7 +88,7 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): nwGUI.close() @pytest.mark.gui -def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -392,7 +392,7 @@ def testDocEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.close() @pytest.mark.gui -def testDocViewer(qtbot, nwLipsum, nwTemp): +def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -544,7 +544,7 @@ def testDocViewer(qtbot, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testProjectTree(qtbot, nwMinimal, nwTemp): +def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) qtbot.addWidget(nwGUI) @@ -678,7 +678,7 @@ def testProjectTree(qtbot, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testEditFormatMenu(qtbot, nwLipsum, nwTemp): +def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -915,7 +915,7 @@ def testEditFormatMenu(qtbot, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testContextMenu(qtbot, nwLipsum, nwTemp): +def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -1087,7 +1087,9 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): assert nwGUI.docEditor.getText() == " " nwGUI.docEditor.clear() - # Insert text from file + ## + # Insert text from file + ## nwGUI.closeDocument() # First, with no path @@ -1123,12 +1125,33 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI.mainMenu.aImportFile.activate(QAction.Trigger) assert nwGUI.docEditor.getText() == "Foo" + ## + # Reveal file location + ## + + theMessage = "" + + def recordMsg(*args): + nonlocal theMessage + theMessage = args[3] + return None + + assert not theMessage + monkeypatch.setattr(QMessageBox, "information", recordMsg) + nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger) + + theBits = theMessage.split("
") + assert len(theBits) == 3 + assert theBits[0] == "File details for the currently open file" + assert theBits[1] == "Handle: 0e17daca5f3e1" + assert theBits[2] == "Location: %s" % path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") + # qtbot.stopForInteraction() nwGUI.closeMain() nwGUI.close() @pytest.mark.gui -def testTextSearch(qtbot, nwLipsum, nwTemp): +def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -1153,8 +1176,9 @@ def testTextSearch(qtbot, nwLipsum, nwTemp): assert nwGUI.docEditor.docSearch.isVisible() assert nwGUI.docEditor.docSearch.getSearchText() == "est" - # Find Next by Menu - nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) + # Find Next by Enter + monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: True) + qtbot.keyClick(nwGUI.docEditor.docSearch.searchBox, Qt.Key_Return, delay=keyDelay) assert abs(nwGUI.docEditor.getCursorPosition() - 1272) < 3 # Find Next by Button @@ -1283,12 +1307,31 @@ def testTextSearch(qtbot, nwLipsum, nwTemp): nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(nwGUI.docEditor.getCursorPosition() - 1127) < 3 + # Toggle Replace + nwGUI.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 + monkeypatch.setattr(nwGUI.docEditor, "hasFocus", lambda: True) + monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: False) + monkeypatch.setattr(nwGUI.docEditor.docSearch.replaceBox, "hasFocus", lambda: False) + assert not nwGUI.docEditor.focusNextPrevChild(True) + + monkeypatch.setattr(nwGUI.docEditor, "hasFocus", lambda: False) + monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: True) + monkeypatch.setattr(nwGUI.docEditor.docSearch.replaceBox, "hasFocus", lambda: False) + assert nwGUI.docEditor.focusNextPrevChild(True) + + monkeypatch.setattr(nwGUI.docEditor.docSearch.searchBox, "hasFocus", lambda: False) + monkeypatch.setattr(nwGUI.docEditor.docSearch.replaceBox, "hasFocus", lambda: True) + assert nwGUI.docEditor.focusNextPrevChild(True) + # qtbot.stopForInteraction() nwGUI.closeMain() nwGUI.close() @pytest.mark.gui -def testOutline(qtbot, nwLipsum, nwTemp): +def testOutline(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) @@ -1349,7 +1392,7 @@ def testOutline(qtbot, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testThemes(qtbot, nwMinimal, nwTemp): +def testThemes(qtbot, yesToAll, nwMinimal, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) qtbot.addWidget(nwGUI) From 5152d80ac48ac45e4d1a7f767da0df2cb499815a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Sep 2020 23:39:43 +0200 Subject: [PATCH 29/51] Fixed a bug in project open where old projects were converted before asking permision --- nw/core/project.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index f7c18bdd..f914bfb5 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -390,16 +390,11 @@ class NWProject(): # Check for Old Legacy Data # ========================= - errList = [] + legacyList = [] # Cleanup is done later for projItem in listdir(self.projPath): logger.verbose("Project contains: %s" % projItem) if projItem.startswith("data_"): - errList = self._legacyDataFolder(projItem, errList) - - if errList: - self.makeAlert(errList, nwAlert.ERROR) - - self._deprecatedFiles() + legacyList.append(projItem) # Project Lock # ============ @@ -611,6 +606,17 @@ class NWProject(): self.optState.loadSettings() + # Sort out old file locations + if legacyList: + errList = [] + for projItem in legacyList: + errList = self._legacyDataFolder(projItem, errList) + if errList: + self.makeAlert(errList, nwAlert.ERROR) + + # Clean up old files + self._deprecatedFiles() + # Update recent projects self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.saveRecentCache() From 9f8dd4abf3f756516fd6ebae8dd61f4c1899e505 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 28 Sep 2020 23:40:49 +0200 Subject: [PATCH 30/51] Monkeypatched a couple more file dialogs --- nw/gui/custom.py | 2 ++ nw/gui/projload.py | 27 ++++++++--------- nw/gui/writingstats.py | 19 ++++++------ tests/reference/novelwriter_prefs.conf | 2 +- tests/test_dialogs.py | 42 ++++++++++++++++++++------ 5 files changed, 58 insertions(+), 34 deletions(-) diff --git a/nw/gui/custom.py b/nw/gui/custom.py index e4e9f0c9..03fc6318 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -443,6 +443,8 @@ class VerticalTabBar(QTabBar): class QuotesDialog(QDialog): + selectedQuote = "" + def __init__(self, theParent=None, currentQuote="\""): QDialog.__init__(self, parent=theParent) diff --git a/nw/gui/projload.py b/nw/gui/projload.py index c19175be..200d8a93 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -178,20 +178,19 @@ class GuiProjectLoad(QDialog): """Browse for a folder path. """ logger.verbose("GuiProjectLoad browse button clicked") - if self.mainConf.showGUI: - dlgOpt = QFileDialog.Options() - dlgOpt |= QFileDialog.DontUseNativeDialog - projFile, _ = QFileDialog.getOpenFileName( - self, "Open novelWriter Project", "", - "novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE, - options=dlgOpt - ) - if projFile: - thePath = path.abspath(path.dirname(projFile)) - self.selPath.setText(thePath) - self.openPath = thePath - self.openState = self.OPEN_STATE - self.accept() + dlgOpt = QFileDialog.Options() + dlgOpt |= QFileDialog.DontUseNativeDialog + projFile, _ = QFileDialog.getOpenFileName( + self, "Open novelWriter Project", "", + "novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE, + options=dlgOpt + ) + if projFile: + thePath = path.abspath(path.dirname(projFile)) + self.selPath.setText(thePath) + self.openPath = thePath + self.openState = self.OPEN_STATE + self.accept() return diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index dd272b8b..4c27520e 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -326,16 +326,15 @@ class GuiWritingStats(QDialog): if not path.isdir(saveDir): saveDir = self.mainConf.homePath - if self.mainConf.showGUI: - dlgOpt = QFileDialog.Options() - dlgOpt |= QFileDialog.DontUseNativeDialog - saveTo = QFileDialog.getSaveFileName( - self, "Save Document As", savePath, options=dlgOpt - ) - if saveTo[0]: - savePath = saveTo[0] - else: - return False + dlgOpt = QFileDialog.Options() + dlgOpt |= QFileDialog.DontUseNativeDialog + saveTo = QFileDialog.getSaveFileName( + self, "Save Document As", savePath, options=dlgOpt + ) + if saveTo: + savePath = saveTo[0] + else: + return False self.mainConf.setLastPath(savePath) diff --git a/tests/reference/novelwriter_prefs.conf b/tests/reference/novelwriter_prefs.conf index 6b894f55..608116ea 100644 --- a/tests/reference/novelwriter_prefs.conf +++ b/tests/reference/novelwriter_prefs.conf @@ -49,7 +49,7 @@ highlightquotes = False highlightemph = False [Backup] -backuppath = +backuppath = some/dir backuponclose = True askbeforebackup = True diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 464b39bc..0076e181 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -14,7 +14,7 @@ from os import path from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtWidgets import ( QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog, QAction, - QMessageBox, QFileDialog + QMessageBox, QFileDialog, QFontDialog ) from nw.gui import ( @@ -198,7 +198,7 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # qtbot.stopForInteraction() @pytest.mark.gui -def testWritingStatsExport(qtbot, yesToAll, nwFuncTemp, nwTemp): +def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -263,6 +263,10 @@ def testWritingStatsExport(qtbot, yesToAll, nwFuncTemp, nwTemp): assert isinstance(sessLog, GuiWritingStats) qtbot.wait(stepDelay) + monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda *args, **kwargs: []) + assert not sessLog._saveData(sessLog.FMT_CSV) + + monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: [pp]) assert sessLog._saveData(sessLog.FMT_CSV) qtbot.wait(stepDelay) assert sessLog._saveData(sessLog.FMT_JSON) @@ -913,9 +917,16 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwLoad._keyPressDelete() assert nwLoad.listBox.topLevelItemCount() == recentCount - 1 - nwLoad.close() + getFile = path.join(nwMinimal, "nwProject.nwx") + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwargs: (getFile, None)) + qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton) + assert nwLoad.openPath == nwMinimal + assert nwLoad.openState == nwLoad.OPEN_STATE # qtbot.stopForInteraction() + + nwLoad.close() nwGUI.closeMain() + nwGUI.close() @pytest.mark.gui def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf): @@ -927,8 +938,6 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC assert nwGUI.openProject(nwMinimal) - monkeypatch.setattr(QMessageBox, "information", lambda *args, **kwargs: None) - monkeypatch.setattr(GuiPreferences, "exec_", lambda *args: None) monkeypatch.setattr(GuiPreferences, "result", lambda *args: QDialog.Accepted) nwGUI.mainMenu.aPreferences.activate(QAction.Trigger) @@ -952,7 +961,7 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.wait(keyDelay) tabGeneral = nwPrefs.tabGeneral nwPrefs._tabBox.setCurrentWidget(tabGeneral) - tabGeneral.backupPath = nwTemp + tabGeneral.backupPath = "no/where" qtbot.wait(keyDelay) assert not tabGeneral.preferDarkIcons.isChecked() @@ -964,6 +973,16 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC qtbot.mouseClick(tabGeneral.showFullPath, Qt.LeftButton) assert not tabGeneral.showFullPath.isChecked() + # Check Browse button + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "") + assert not tabGeneral._backupFolder() + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "some/dir") + qtbot.mouseClick(tabGeneral.backupGetPath, Qt.LeftButton) + + # Check font button + monkeypatch.setattr(QFontDialog, "getFont", lambda font, obj: (font, True)) + qtbot.mouseClick(tabGeneral.fontButton, Qt.LeftButton) + qtbot.wait(keyDelay) assert not tabGeneral.backupOnClose.isChecked() qtbot.mouseClick(tabGeneral.backupOnClose, Qt.LeftButton) @@ -979,6 +998,9 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC tabLayout = nwPrefs.tabLayout nwPrefs._tabBox.setCurrentWidget(tabLayout) + qtbot.wait(keyDelay) + qtbot.mouseClick(tabLayout.fontButton, Qt.LeftButton) + qtbot.wait(keyDelay) tabLayout.textStyleSize.setValue(13) tabLayout.textFlowMax.setValue(700) @@ -1050,12 +1072,14 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC assert not tabAutoRep.autoReplaceDash.isEnabled() assert not tabAutoRep.autoReplaceDots.isEnabled() + monkeypatch.setattr(QuotesDialog, "selectedQuote", "'") + monkeypatch.setattr(QuotesDialog, "exec_", lambda *args: QDialog.Accepted) + qtbot.mouseClick(tabAutoRep.btnDoubleStyleC, Qt.LeftButton) + # Save and Check Config qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) assert tmpConf.confChanged - assert tmpConf.backupPath == nwTemp - tmpConf.backupPath = "" tmpConf.lastPath = "" assert nwGUI.mainConf.saveConfig() @@ -1070,7 +1094,7 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC ignoreLines = [ 2, # Timestamp 11, 12, 13, 14, 15, 16, 17, # Window sizes - 7, 25, # Fonts (depends in system default) + 7, 25, # Fonts (depends on system default) ] assert cmpFiles(testConf, refConf, ignoreLines) From 0c9849b9066be794b1aea8d7aa576f2e5217af66 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Sep 2020 21:10:21 +0200 Subject: [PATCH 31/51] Various test changes, and improvements to project wizard test --- .gitignore | 8 +- tests/conftest.py | 2 +- tests/minimal/meta/guiOptions.json | 1 - tests/minimal/meta/sessionStats.log | 2 - tests/minimal/meta/tagsIndex.json | 91 ----------- tests/nwtools.py | 13 +- tests/test_dialogs.py | 72 ++++++--- tests/test_gui.py | 61 ++++---- tests/test_index.py | 226 ++++++++++++++++++++++++++++ tests/test_project.py | 218 --------------------------- 10 files changed, 319 insertions(+), 375 deletions(-) delete mode 100644 tests/minimal/meta/guiOptions.json delete mode 100644 tests/minimal/meta/sessionStats.log delete mode 100644 tests/minimal/meta/tagsIndex.json create mode 100644 tests/test_index.py diff --git a/.gitignore b/.gitignore index fdfe7e87..57dc50a9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,12 @@ # Documentation /docs/build/ -novelWriter.qch -novelWriter.qhc +*.qch +*.qhc # Python Temp __pycache__ +*.pyc # Sample Project /nw/assets/sample.zip @@ -27,6 +28,9 @@ __pycache__ /tests/temp /tests/lipsum/cache /tests/lipsum/meta +/tests/minimal/cache +/tests/minimal/meta +/tests/oldproj/cache /.pytest_cache /pytestdebug.log diff --git a/tests/conftest.py b/tests/conftest.py index bafa1fbb..8967ca5d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,7 +185,7 @@ def nwOldProj(nwTemp): @pytest.fixture(scope="function") def yesToAll(monkeypatch): - """Make the message boxes/questions always say yes to the dress! + """Make the message boxes/questions always say yes. """ monkeypatch.setattr( QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes diff --git a/tests/minimal/meta/guiOptions.json b/tests/minimal/meta/guiOptions.json deleted file mode 100644 index 9e26dfee..00000000 --- a/tests/minimal/meta/guiOptions.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/tests/minimal/meta/sessionStats.log b/tests/minimal/meta/sessionStats.log deleted file mode 100644 index 38cbde3d..00000000 --- a/tests/minimal/meta/sessionStats.log +++ /dev/null @@ -1,2 +0,0 @@ -# Start Time End Time Novel Notes -2020-08-28 11:36:36 2020-08-28 11:36:48 10 0 diff --git a/tests/minimal/meta/tagsIndex.json b/tests/minimal/meta/tagsIndex.json deleted file mode 100644 index ec600a1c..00000000 --- a/tests/minimal/meta/tagsIndex.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "tagIndex": {}, - "refIndex": { - "a35baf2e93843": { - "T000000": { - "tags": [], - "updated": 1598607396 - }, - "T000001": { - "tags": [], - "updated": 1598607396 - } - }, - "f5ab3e30151e1": { - "T000000": { - "tags": [], - "updated": 1598607396 - }, - "T000001": { - "tags": [], - "updated": 1598607396 - } - }, - "8c659a11cd429": { - "T000000": { - "tags": [], - "updated": 1598607396 - }, - "T000001": { - "tags": [], - "updated": 1598607396 - } - } - }, - "novelIndex": { - "a35baf2e93843": { - "T000001": { - "level": "H1", - "title": "Minimal", - "layout": "TITLE", - "synopsis": "", - "cCount": 7, - "wCount": 1, - "pCount": 0, - "updated": 1598607396 - } - }, - "f5ab3e30151e1": { - "T000001": { - "level": "H2", - "title": "New Chapter", - "layout": "CHAPTER", - "synopsis": "", - "cCount": 11, - "wCount": 2, - "pCount": 0, - "updated": 1598607396 - } - }, - "8c659a11cd429": { - "T000001": { - "level": "H3", - "title": "New Scene", - "layout": "SCENE", - "synopsis": "", - "cCount": 9, - "wCount": 2, - "pCount": 0, - "updated": 1598607396 - } - } - }, - "noteIndex": {}, - "textCounts": { - "a35baf2e93843": [ - 28, - 6, - 1 - ], - "f5ab3e30151e1": [ - 11, - 2, - 0 - ], - "8c659a11cd429": [ - 9, - 2, - 0 - ] - } -} \ No newline at end of file diff --git a/tests/nwtools.py b/tests/nwtools.py index 0a9516aa..0b8896a3 100644 --- a/tests/nwtools.py +++ b/tests/nwtools.py @@ -2,18 +2,13 @@ """novelWriter Test Tools """ -from os import path, mkdir from itertools import chain from PyQt5.QtWidgets import qApp -def ensureDir(theDir): - if not path.isdir(theDir): - mkdir(theDir) - return - def cmpFiles(fileOne, fileTwo, ignoreLines=[]): - + """Compare two files, but optionally ignore lines given by a list. + """ try: foOne = open(fileOne, mode="r", encoding="utf8") except Exception as e: @@ -54,6 +49,8 @@ def cmpFiles(fileOne, fileTwo, ignoreLines=[]): return not diffFound def cmpList(listOne, listTwo): + """Compare two iterable objects. + """ flatOne = list(chain.from_iterable([listOne])) flatTwo = list(chain.from_iterable([listTwo])) if len(flatOne) != len(flatTwo): @@ -64,6 +61,8 @@ def cmpList(listOne, listTwo): return True def getGuiItem(theName): + """Returns a QtWidget based on its objectName. + """ for qWidget in qApp.topLevelWidgets(): if qWidget.objectName() == theName: return qWidget diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 0076e181..40970d2f 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -26,6 +26,7 @@ from nw.gui.custom import QuotesDialog from nw.constants import nwItemType, nwItemLayout, nwItemClass keyDelay = 2 +typeDelay = 1 stepDelay = 20 @pytest.mark.gui @@ -60,14 +61,14 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR qtbot.wait(stepDelay) projEdit.tabMain.editName.setText("") for c in "Project Name": - qtbot.keyClick(projEdit.tabMain.editName, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabMain.editName, c, delay=typeDelay) for c in "Project Title": - qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=typeDelay) for c in "Jane Doe": - qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=typeDelay) qtbot.keyClick(projEdit.tabMain.editAuthors, Qt.Key_Return, delay=keyDelay) for c in "John Doh": - qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=typeDelay) # Test Status Tab qtbot.wait(stepDelay) @@ -77,9 +78,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR qtbot.mouseClick(projEdit.tabStatus.newButton, Qt.LeftButton) projEdit.tabStatus.listBox.item(3).setSelected(True) for n in range(8): - qtbot.keyClick(projEdit.tabStatus.editName, Qt.Key_Backspace, delay=keyDelay) + qtbot.keyClick(projEdit.tabStatus.editName, Qt.Key_Backspace, delay=typeDelay) for c in "Final": - qtbot.keyClick(projEdit.tabStatus.editName, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabStatus.editName, c, delay=typeDelay) qtbot.mouseClick(projEdit.tabStatus.saveButton, Qt.LeftButton) # Auto-Replace Tab @@ -89,9 +90,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR qtbot.mouseClick(projEdit.tabReplace.addButton, Qt.LeftButton) projEdit.tabReplace.listBox.topLevelItem(0).setSelected(True) for c in "Th is ": - qtbot.keyClick(projEdit.tabReplace.editKey, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabReplace.editKey, c, delay=typeDelay) for c in "With This Stuff ": - qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=keyDelay) + qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=typeDelay) qtbot.mouseClick(projEdit.tabReplace.saveButton, Qt.LeftButton) qtbot.wait(stepDelay) @@ -159,7 +160,7 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert itemEdit.editLayout.currentData() == nwItemLayout.SCENE for c in "Just a Page": - qtbot.keyClick(itemEdit.editName, c, delay=keyDelay) + qtbot.keyClick(itemEdit.editName, c, delay=typeDelay) itemEdit.editStatus.setCurrentIndex(1) layoutIdx = itemEdit.editLayout.findData(nwItemLayout.PAGE) itemEdit.editLayout.setCurrentIndex(layoutIdx) @@ -747,8 +748,11 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) + nwGUI.mainConf.lastPath = " " - for wStep in range(3): + for wStep in range(4): + # This does not actually create the project, it just generates the + # dictionary that defines it. # The Wizard nwWiz = GuiProjectWizard(nwGUI) @@ -762,15 +766,15 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): qtbot.wait(stepDelay) for c in "Test Minimal": - qtbot.keyClick(introPage.projName, c, delay=keyDelay) + qtbot.keyClick(introPage.projName, c, delay=typeDelay) qtbot.wait(stepDelay) for c in "Minimal Novel": - qtbot.keyClick(introPage.projTitle, c, delay=keyDelay) + qtbot.keyClick(introPage.projTitle, c, delay=typeDelay) qtbot.wait(stepDelay) for c in "Jane Doe": - qtbot.keyClick(introPage.projAuthors, c, delay=keyDelay) + qtbot.keyClick(introPage.projAuthors, c, delay=typeDelay) # Setting projName should activate the button assert nwWiz.button(QWizard.NextButton).isEnabled() @@ -783,10 +787,20 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): assert isinstance(storagePage, ProjWizardFolderPage) assert not nwWiz.button(QWizard.NextButton).isEnabled() + if wStep == 0: + # Check invalid path first, the first time we reach here + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: "") + qtbot.wait(stepDelay) + qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) + assert storagePage.projPath.text() == "" + + # Then, we always return nwMinimal as path + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: nwMinimal) + qtbot.wait(stepDelay) - projPath = path.join(nwTemp, "dummy") - for c in projPath: - qtbot.keyClick(storagePage.projPath, c, delay=keyDelay) + qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) + projPath = path.join(nwMinimal, "Test Minimal") + assert storagePage.projPath.text() == projPath # Setting projPath should activate the button assert nwWiz.button(QWizard.NextButton).isEnabled() @@ -805,13 +819,15 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): elif wStep == 1: popPage.popCustom.setChecked(True) elif wStep == 2: + popPage.popCustom.setChecked(True) + elif wStep == 3: popPage.popSample.setChecked(True) qtbot.wait(stepDelay) qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) # Custom Page - if wStep == 1: + if wStep == 1 or wStep == 2: customPage = nwWiz.currentPage() assert isinstance(customPage, ProjWizardCustomPage) assert nwWiz.button(QWizard.NextButton).isEnabled() @@ -823,6 +839,11 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): customPage.addObject.setChecked(True) customPage.addEntity.setChecked(True) + if wStep == 2: + customPage.numChapters.setValue(0) + customPage.numScenes.setValue(10) + customPage.chFolders.setChecked(False) + qtbot.wait(stepDelay) qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton) @@ -839,9 +860,9 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): assert projData["projAuthors"] == "Jane Doe" assert projData["projPath"] == projPath assert projData["popMinimal"] == (wStep == 0) - assert projData["popCustom"] == (wStep == 1) - assert projData["popSample"] == (wStep == 2) - if wStep == 1: + assert projData["popCustom"] == (wStep == 1 or wStep == 2) + assert projData["popSample"] == (wStep == 3) + if wStep == 1 or wStep == 2: assert projData["addRoots"] == [ nwItemClass.PLOT, nwItemClass.CHARACTER, @@ -850,9 +871,14 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwItemClass.OBJECT, nwItemClass.ENTITY, ] - assert projData["numChapters"] == 5 - assert projData["numScenes"] == 5 - assert projData["chFolders"] + if wStep == 1: + assert projData["numChapters"] == 5 + assert projData["numScenes"] == 5 + assert projData["chFolders"] + else: + assert projData["numChapters"] == 0 + assert projData["numScenes"] == 10 + assert not projData["chFolders"] else: assert projData["addRoots"] == [] assert projData["numChapters"] == 0 diff --git a/tests/test_gui.py b/tests/test_gui.py index 5289b187..85c09982 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -21,6 +21,7 @@ from nw.constants import ( ) keyDelay = 2 +typeDelay = 1 stepDelay = 20 @pytest.mark.gui @@ -165,15 +166,15 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.setFocus(2) qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay) for c in "# Jane Doe": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@tag: Jane": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "This is a file about Jane.": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) # Add a Plot File @@ -187,15 +188,15 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.setFocus(2) qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay) for c in "# Main Plot": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@tag: MainPlot": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "This is a file detailing the main plot.": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) # Add a World File @@ -214,15 +215,15 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.setFocus(2) qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay) for c in "# Main Location": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@tag: Home": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "This is a file describing Jane's home.": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) # Trigger autosaves before making more changes @@ -241,54 +242,54 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.setFocus(2) qtbot.keyClick(nwGUI.docEditor, "a", modifier=Qt.ControlModifier, delay=keyDelay) for c in "# Novel": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "## Chapter": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@pov: Jane": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@plot: MainPlot": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "### Scene": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "% How about a comment?": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@pov: Jane": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@plot: MainPlot": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@location: Home": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "#### Some Section": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "@char: Jane": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "This is a paragraph of dummy text.": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) @@ -296,32 +297,32 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): "This is another paragraph of much longer dummy text. " "It is in fact very very dumb dummy text! " ): - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) for c in "We can also try replacing \"quotes\", even single 'quotes' are replaced. ": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) for c in "Isn't that nice? ": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) for c in "Ellipsis? Not a problem either ... ": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) for c in "How about three hyphens - -": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Left, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Backspace, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Right, delay=keyDelay) for c in "- for long dash? It works too.": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "\"Full line double quoted text.\"": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) for c in "'Full line single quoted text.'": - qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, c, delay=typeDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) diff --git a/tests/test_index.py b/tests/test_index.py new file mode 100644 index 00000000..ff3e9260 --- /dev/null +++ b/tests/test_index.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Class Tester +""" + +import pytest + +from nw.core.project import NWProject +from nw.core.index import NWIndex +from nw.constants import nwItemClass + +@pytest.mark.project +def testIndexScanThis(nwMinimal, nwDummy): + + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwMinimal) + + theIndex = NWIndex(theProject, nwDummy) + + isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") + assert not isValid + + isValid, theBits, thePos = theIndex.scanThis("@") + assert not isValid + + isValid, theBits, thePos = theIndex.scanThis("@:") + assert not isValid + + isValid, theBits, thePos = theIndex.scanThis(" @a: b") + assert not isValid + + isValid, theBits, thePos = theIndex.scanThis("@a:") + assert isValid + assert str(theBits) == "['@a']" + assert str(thePos) == "[0]" + + isValid, theBits, thePos = theIndex.scanThis("@a:b") + assert isValid + assert str(theBits) == "['@a', 'b']" + assert str(thePos) == "[0, 3]" + + isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d") + assert isValid + assert str(theBits) == "['@a', 'b', 'c', 'd']" + assert str(thePos) == "[0, 3, 5, 7]" + + isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d") + assert isValid + assert str(theBits) == "['@a', 'b', 'c', 'd']" + assert str(thePos) == "[0, 5, 9, 13]" + + isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this") + assert isValid + assert str(theBits) == "['@tag', 'this', 'and this']" + assert str(thePos) == "[0, 6, 12]" + + assert theProject.closeProject() + +@pytest.mark.project +def testIndexCheckThese(nwMinimal, nwDummy): + + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwMinimal) + + theIndex = NWIndex(theProject, nwDummy) + nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") + cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + nItem = theProject.projTree[nHandle] + cItem = theProject.projTree[cHandle] + + assert theIndex.scanText(cHandle, ( + "# Jane Smith\n" + "@tag: Jane" + )) + assert theIndex.scanText(nHandle, ( + "# Hello World!\n" + "@pov: Jane" + )) + assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle + assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" + + assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]" + assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]" + assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]" + assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]" + assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]" + assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]" + assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]" + assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]" + + assert theProject.closeProject() + +@pytest.mark.project +def testIndexMeta(nwMinimal, nwDummy): + + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwMinimal) + + theIndex = NWIndex(theProject, nwDummy) + nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") + cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + + assert theIndex.scanText(cHandle, ( + "# Jane Smith\n" + "@tag: Jane\n" + )) + assert theIndex.scanText(nHandle, ( + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + )) + assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle + assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" + + # The novel structure should contain the pointer to the novel file header + assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle + + # The novel file should have the correct counts + cC, wC, pC = theIndex.getCounts(nHandle) + assert cC == 62 # Characters in text and title only + assert wC == 12 # Words in text and title only + assert pC == 2 # Paragraphs in text only + + # Look up an ivalid handle + theRefs = theIndex.getReferences("Not a handle") + assert theRefs["@pov"] == [] + assert theRefs["@char"] == [] + + # The novel file should now refer to Jane as @pov and @char + theRefs = theIndex.getReferences(nHandle) + assert str(theRefs["@pov"]) == "['Jane']" + assert str(theRefs["@char"]) == "['Jane']" + + # The character file should have a record of the reference from the novel file + theRefs = theIndex.getBackReferenceList(cHandle) + assert str(theRefs) == "{'%s': 'T000001'}" % nHandle + + # Get section counts for a novel file + assert theIndex.scanText(nHandle, ( + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + "\n" + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + )) + # Whole document + cC, wC, pC = theIndex.getCounts(nHandle) + assert cC == 124 + assert wC == 24 + assert pC == 4 + + # First part + cC, wC, pC = theIndex.getCounts(nHandle, "T000001") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + # First part + cC, wC, pC = theIndex.getCounts(nHandle, "T000011") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + # Get section counts for a note file + assert theIndex.scanText(cHandle, ( + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + "\n" + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n" + "\n" + "% this is a comment\n" + "\n" + "This is a story about Jane Smith.\n" + "\n" + "Well, not really.\n" + )) + # Whole document + cC, wC, pC = theIndex.getCounts(cHandle) + assert cC == 124 + assert wC == 24 + assert pC == 4 + + # First part + cC, wC, pC = theIndex.getCounts(cHandle, "T000001") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + # First part + cC, wC, pC = theIndex.getCounts(cHandle, "T000011") + assert cC == 62 + assert wC == 12 + assert pC == 2 + + assert theProject.closeProject() diff --git a/tests/test_project.py b/tests/test_project.py index 9ee7324c..8656ad89 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -11,7 +11,6 @@ from nwtools import cmpFiles from nw.core.project import NWProject from nw.core.document import NWDoc -from nw.core.index import NWIndex from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles @@ -109,223 +108,6 @@ def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy): assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) assert not theProject.projChanged -@pytest.mark.project -def testIndexScanThis(nwMinimal, nwDummy): - - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - assert theProject.openProject(nwMinimal) - - theIndex = NWIndex(theProject, nwDummy) - - isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") - assert not isValid - - isValid, theBits, thePos = theIndex.scanThis("@") - assert not isValid - - isValid, theBits, thePos = theIndex.scanThis("@:") - assert not isValid - - isValid, theBits, thePos = theIndex.scanThis(" @a: b") - assert not isValid - - isValid, theBits, thePos = theIndex.scanThis("@a:") - assert isValid - assert str(theBits) == "['@a']" - assert str(thePos) == "[0]" - - isValid, theBits, thePos = theIndex.scanThis("@a:b") - assert isValid - assert str(theBits) == "['@a', 'b']" - assert str(thePos) == "[0, 3]" - - isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d") - assert isValid - assert str(theBits) == "['@a', 'b', 'c', 'd']" - assert str(thePos) == "[0, 3, 5, 7]" - - isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d") - assert isValid - assert str(theBits) == "['@a', 'b', 'c', 'd']" - assert str(thePos) == "[0, 5, 9, 13]" - - isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this") - assert isValid - assert str(theBits) == "['@tag', 'this', 'and this']" - assert str(thePos) == "[0, 6, 12]" - - assert theProject.closeProject() - -@pytest.mark.project -def testIndexCheckThese(nwMinimal, nwDummy): - - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - assert theProject.openProject(nwMinimal) - - theIndex = NWIndex(theProject, nwDummy) - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") - nItem = theProject.projTree[nHandle] - cItem = theProject.projTree[cHandle] - - assert theIndex.scanText(cHandle, ( - "# Jane Smith\n" - "@tag: Jane" - )) - assert theIndex.scanText(nHandle, ( - "# Hello World!\n" - "@pov: Jane" - )) - assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle - assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" - - assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]" - assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]" - assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]" - assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]" - assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]" - assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]" - assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]" - assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]" - - assert theProject.closeProject() - -@pytest.mark.project -def testIndexMeta(nwMinimal, nwDummy): - - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - assert theProject.openProject(nwMinimal) - - theIndex = NWIndex(theProject, nwDummy) - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") - - assert theIndex.scanText(cHandle, ( - "# Jane Smith\n" - "@tag: Jane\n" - )) - assert theIndex.scanText(nHandle, ( - "# Hello World!\n" - "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - )) - assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle - assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" - - # The novel structure should contain the pointer to the novel file header - assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle - - # The novel file should have the correct counts - cC, wC, pC = theIndex.getCounts(nHandle) - assert cC == 62 # Characters in text and title only - assert wC == 12 # Words in text and title only - assert pC == 2 # Paragraphs in text only - - # Look up an ivalid handle - theRefs = theIndex.getReferences("Not a handle") - assert theRefs["@pov"] == [] - assert theRefs["@char"] == [] - - # The novel file should now refer to Jane as @pov and @char - theRefs = theIndex.getReferences(nHandle) - assert str(theRefs["@pov"]) == "['Jane']" - assert str(theRefs["@char"]) == "['Jane']" - - # The character file should have a record of the reference from the novel file - theRefs = theIndex.getBackReferenceList(cHandle) - assert str(theRefs) == "{'%s': 'T000001'}" % nHandle - - # Get section counts for a novel file - assert theIndex.scanText(nHandle, ( - "# Hello World!\n" - "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - "\n" - "# Hello World!\n" - "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - )) - # Whole document - cC, wC, pC = theIndex.getCounts(nHandle) - assert cC == 124 - assert wC == 24 - assert pC == 4 - - # First part - cC, wC, pC = theIndex.getCounts(nHandle, "T000001") - assert cC == 62 - assert wC == 12 - assert pC == 2 - - # First part - cC, wC, pC = theIndex.getCounts(nHandle, "T000011") - assert cC == 62 - assert wC == 12 - assert pC == 2 - - # Get section counts for a note file - assert theIndex.scanText(cHandle, ( - "# Hello World!\n" - "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - "\n" - "# Hello World!\n" - "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - )) - # Whole document - cC, wC, pC = theIndex.getCounts(cHandle) - assert cC == 124 - assert wC == 24 - assert pC == 4 - - # First part - cC, wC, pC = theIndex.getCounts(cHandle, "T000001") - assert cC == 62 - assert wC == 12 - assert pC == 2 - - # First part - cC, wC, pC = theIndex.getCounts(cHandle, "T000011") - assert cC == 62 - assert wC == 12 - assert pC == 2 - - assert theProject.closeProject() - @pytest.mark.project def testProjectNewCustom(nwFuncTemp, nwTempProj, nwRef, nwDummy): From 0b7cd71b9252dea4a652c7e063b1d324b8464caa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Sep 2020 21:49:00 +0200 Subject: [PATCH 32/51] Complete coverage of new project function and remove test code from main app --- nw/core/project.py | 2 +- nw/guimain.py | 4 +- tests/reference/proj/5_nwProject.nwx | 178 +++++++++++++++++++++++++++ tests/test_dialogs.py | 18 ++- tests/test_gui.py | 8 +- tests/test_project.py | 88 ++++++++++++- 6 files changed, 275 insertions(+), 23 deletions(-) create mode 100644 tests/reference/proj/5_nwProject.nwx diff --git a/nw/core/project.py b/nw/core/project.py index f914bfb5..72ac278d 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -938,7 +938,7 @@ class NWProject(): return False if path.isdir(projPath): - if self.mainConf.showGUI and listdir(self.projPath): + if listdir(self.projPath): self.theParent.makeAlert(( "New project folder is not empty. " "Each project requires a dedicated project folder." diff --git a/nw/guimain.py b/nw/guimain.py index 859145df..44b99d56 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -246,7 +246,7 @@ class GuiMain(QMainWindow): # Project Actions ## - def newProject(self, projData=None, forceNew=False): + def newProject(self, projData=None): """Create new project with a few default files and folders. The variable forceNew is used for testing. """ @@ -268,7 +268,7 @@ class GuiMain(QMainWindow): logger.error("No projData or projPath set") return False - if path.isfile(path.join(projPath, self.theProject.projFile)) and not forceNew: + if path.isfile(path.join(projPath, self.theProject.projFile)): self.makeAlert( "A project already exists in that location. Please choose another folder.", nwAlert.ERROR diff --git a/tests/reference/proj/5_nwProject.nwx b/tests/reference/proj/5_nwProject.nwx new file mode 100644 index 00000000..9935736e --- /dev/null +++ b/tests/reference/proj/5_nwProject.nwx @@ -0,0 +1,178 @@ + + + + Test Custom + Test Novel + Jane Doe + John Doh + 1 + 1 + 0 + + + True + False + None + True + None + None + 0 + 0 + 0 + + + %title% + Chapter %ch%: %title% + %title% + * * * +
+
+ + New + Note + Draft + Finished + + + New + Minor + Major + Main + +
+ + + Novel + ROOT + NOVEL + New + False + + + Plot + ROOT + PLOT + New + False + + + Characters + ROOT + CHARACTER + New + False + + + Locations + ROOT + WORLD + New + False + + + Timeline + ROOT + TIMELINE + New + False + + + Objects + ROOT + OBJECT + New + False + + + Entity + ROOT + ENTITY + New + False + + + Title Page + FILE + NOVEL + New + True + TITLE + 0 + 0 + 0 + 0 + + + Scene 1 + FILE + NOVEL + New + True + SCENE + 0 + 0 + 0 + 0 + + + Scene 2 + FILE + NOVEL + New + True + SCENE + 0 + 0 + 0 + 0 + + + Scene 3 + FILE + NOVEL + New + True + SCENE + 0 + 0 + 0 + 0 + + + Scene 4 + FILE + NOVEL + New + True + SCENE + 0 + 0 + 0 + 0 + + + Scene 5 + FILE + NOVEL + New + True + SCENE + 0 + 0 + 0 + 0 + + + Scene 6 + FILE + NOVEL + New + True + SCENE + 0 + 0 + 0 + 0 + + +
diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 40970d2f..3de991dc 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -31,7 +31,7 @@ stepDelay = 20 @pytest.mark.gui def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -43,7 +43,7 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR # Create new project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}, True) + assert nwGUI.newProject({"projPath": nwFuncTemp}) nwGUI.mainConf.backupPath = nwFuncTemp # Get the dialog object @@ -141,7 +141,7 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR @pytest.mark.gui def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -149,7 +149,7 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # Create new, save, open project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}, True) + assert nwGUI.newProject({"projPath": nwFuncTemp}) assert nwGUI.openDocument("0e17daca5f3e1") itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1") @@ -200,7 +200,7 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): @pytest.mark.gui def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -208,7 +208,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}, True) + assert nwGUI.newProject({"projPath": nwFuncTemp}) assert nwGUI.saveProject() assert nwGUI.closeProject() qtbot.wait(stepDelay) @@ -703,7 +703,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): ProjWizardCustomPage, ProjWizardFinalPage ) - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -730,10 +730,6 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {"projPath": nwMinimal}) assert not nwGUI.newProject() - # Force overwrite - monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {"projPath": nwMinimal}) - assert nwGUI.newProject(forceNew=True) - nwGUI.closeMain() nwGUI.close() diff --git a/tests/test_gui.py b/tests/test_gui.py index 85c09982..c3d02811 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -91,7 +91,7 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): @pytest.mark.gui def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -99,7 +99,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}, True) + assert nwGUI.newProject({"projPath": nwFuncTemp}) assert nwGUI.saveProject() assert nwGUI.closeProject() @@ -1003,14 +1003,14 @@ def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp): @pytest.mark.gui def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}, True) + assert nwGUI.newProject({"projPath": nwFuncTemp}) assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None diff --git a/tests/test_project.py b/tests/test_project.py index 8656ad89..fc980d96 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -16,6 +16,8 @@ from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles @pytest.mark.project def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): + """Test that a basic project can be created, and opened and saved. + """ projFile = path.join(nwFuncTemp, "nwProject.nwx") testFile = path.join(nwTempProj, "1_nwProject.nwx") refFile = path.join(nwRef, "proj", "1_nwProject.nwx") @@ -23,11 +25,18 @@ def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) + # Setting no data should fail + assert not theProject.newProject({}) + + # Try again with a proper path assert theProject.newProject({"projPath": nwFuncTemp}) assert theProject.setProjectPath(nwFuncTemp) assert theProject.saveProject() assert theProject.closeProject() + # Creating the project once more should fail + assert not theProject.newProject({"projPath": nwFuncTemp}) + # Check the new project copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) @@ -53,6 +62,8 @@ def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): @pytest.mark.project def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy): + """Check that new root folders can be added to the project. + """ projFile = path.join(nwFuncTemp, "nwProject.nwx") testFile = path.join(nwTempProj, "2_nwProject.nwx") refFile = path.join(nwRef, "proj", "2_nwProject.nwx") @@ -85,6 +96,8 @@ def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy): @pytest.mark.project def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy): + """Check that new files can be added to the project. + """ projFile = path.join(nwFuncTemp, "nwProject.nwx") testFile = path.join(nwTempProj, "3_nwProject.nwx") refFile = path.join(nwRef, "proj", "3_nwProject.nwx") @@ -109,8 +122,10 @@ def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy): assert not theProject.projChanged @pytest.mark.project -def testProjectNewCustom(nwFuncTemp, nwTempProj, nwRef, nwDummy): - +def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy): + """Create a new project from a project wizard dictionary. + Custom type with chapters and scenes. + """ projFile = path.join(nwFuncTemp, "nwProject.nwx") testFile = path.join(nwTempProj, "4_nwProject.nwx") refFile = path.join(nwRef, "proj", "4_nwProject.nwx") @@ -145,8 +160,50 @@ def testProjectNewCustom(nwFuncTemp, nwTempProj, nwRef, nwDummy): copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) +@pytest.mark.project +def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy): + """Create a new project from a project wizard dictionary. + Custom type without chapters, but with scenes. + """ + projFile = path.join(nwFuncTemp, "nwProject.nwx") + testFile = path.join(nwTempProj, "5_nwProject.nwx") + refFile = path.join(nwRef, "proj", "5_nwProject.nwx") + + projData = { + "projName": "Test Custom", + "projTitle": "Test Novel", + "projAuthors": "Jane Doe\nJohn Doh\n", + "projPath": nwFuncTemp, + "popSample": False, + "popMinimal": False, + "popCustom": True, + "addRoots": [ + nwItemClass.PLOT, + nwItemClass.CHARACTER, + nwItemClass.WORLD, + nwItemClass.TIMELINE, + nwItemClass.OBJECT, + nwItemClass.ENTITY, + ], + "numChapters": 0, + "numScenes": 6, + "chFolders": True, + } + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + + assert theProject.newProject(projData) + assert theProject.saveProject() + assert theProject.closeProject() + + copyfile(projFile, testFile) + assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) + @pytest.mark.project def testProjectNewSample(nwFuncTemp, nwRef, nwConf, nwDummy): + """Check that we can create a new project can be created from the + provided sample project. + """ projData = { "projName": "Test Sample", "projTitle": "Test Novel", @@ -168,6 +225,8 @@ def testProjectNewSample(nwFuncTemp, nwRef, nwConf, nwDummy): @pytest.mark.project def testDocMeta(nwDummy, nwLipsum): + """Check that the document meta data string is parsed correctly. + """ theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) assert theProject.openProject(nwLipsum) @@ -243,6 +302,10 @@ def testSpellSimple(nwTemp, nwConf): @pytest.mark.project def testProjectOptions(nwDummy, nwLipsum): + """Test the class that holds all the GUI state user options that are + tied to the current open project. Non-project related GUI options + are handled by the Config class. + """ theProject = NWProject(nwDummy) assert theProject.projMeta is None @@ -301,7 +364,12 @@ def testProjectOptions(nwDummy, nwLipsum): assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False @pytest.mark.project -def testOrphanedFiles(nwDummy, nwLipsum): +def testProjectOrphanedFiles(nwDummy, nwLipsum): + """Check that files in the content folder that are not tracked in + the project XML file are handled correctly by the orphaned files + function. It should also restore as much meta data as possible from + the meta line at the top of the document file. + """ theProject = NWProject(nwDummy) assert theProject.openProject(nwLipsum) assert theProject.projTree["636b6aa9b697b"] is None @@ -362,7 +430,12 @@ def testOrphanedFiles(nwDummy, nwLipsum): assert theProject.closeProject() @pytest.mark.project -def testOldProject(nwDummy, nwOldProj): +def testProjectOldFormat(nwDummy, nwOldProj): + """Test that a project folder structure of version 1.0 can be + converted to the latest folder structure. Version 1.0 split the + documents into 'data_0' ... 'data_f' folders, which are now all + contained in a single 'content' folder. + """ theProject = NWProject(nwDummy) theProject.mainConf.showGUI = False @@ -448,7 +521,12 @@ def testOldProject(nwDummy, nwOldProj): assert path.isfile(path.join(nwOldProj, "ToC.txt")) @pytest.mark.project -def testBackupProject(nwDummy, nwMinimal, nwTemp): +def testProjectBackup(nwDummy, nwMinimal, nwTemp): + """Test the automated backup feature of the project class. The test + creates a backup of the Minimal test project, and then unzips the + backupd file and checks that the project XML file is identical to + the original file. + """ theProject = NWProject(nwDummy) assert theProject.openProject(nwMinimal) From e251ddacd2d87cac80d26aadbbb08433adb1c852 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Sep 2020 23:27:51 +0200 Subject: [PATCH 33/51] Completed coverage of setProjectPath and made all os imports explicit --- nw/__init__.py | 11 +- nw/config.py | 70 ++++++------ nw/core/document.py | 25 ++-- nw/core/index.py | 8 +- nw/core/options.py | 9 +- nw/core/project.py | 183 +++++++++++++++--------------- nw/core/spellcheck.py | 10 +- nw/core/tree.py | 12 +- nw/gui/about.py | 6 +- nw/gui/build.py | 12 +- nw/gui/preferences.py | 5 +- nw/gui/projload.py | 4 +- nw/gui/projwizard.py | 7 +- nw/gui/theme.py | 76 ++++++------- nw/gui/writingstats.py | 8 +- nw/guimain.py | 4 +- setup.py | 5 +- tests/conftest.py | 92 +++++++-------- tests/profilestats.py | 7 +- tests/test_config.py | 31 ++--- tests/test_dialogs.py | 153 +++++++++++++------------ tests/test_gui.py | 62 +++++----- tests/test_project.py | 251 ++++++++++++++++++++++++++++------------- 23 files changed, 565 insertions(+), 486 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index e9432711..eca0e5ee 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -28,8 +28,7 @@ import sys import getopt import logging - -from os import path, remove, rename +import os from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage @@ -203,10 +202,10 @@ def main(sysArgs=None): logFmt = logging.Formatter(fmt=logFormat, style="{") if not logFile == "" and toFile: - if path.isfile(logFile+".bak"): - remove(logFile+".bak") - if path.isfile(logFile): - rename(logFile, logFile+".bak") + if os.path.isfile(logFile+".bak"): + os.remove(logFile+".bak") + if os.path.isfile(logFile): + os.rename(logFile, logFile+".bak") fHandle = logging.FileHandler(logFile) fHandle.setLevel(debugLevel) diff --git a/nw/config.py b/nw/config.py index db8a6c17..c5512783 100644 --- a/nw/config.py +++ b/nw/config.py @@ -29,8 +29,8 @@ import logging import configparser import json import sys +import os -from os import path, mkdir, unlink, rename from time import time from shutil import which @@ -238,7 +238,7 @@ class Config: logger.debug("Initialising Config ...") if confPath is None: confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation) - self.confPath = path.join(path.abspath(confRoot), self.appHandle) + self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle) else: logger.info("Setting config from alternative path: %s" % confPath) self.confPath = confPath @@ -248,7 +248,7 @@ class Config: dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) else: dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation) - self.dataPath = path.join(path.abspath(dataRoot), self.appHandle) + self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle) else: logger.info("Setting data path from alternative path: %s" % dataPath) self.dataPath = dataPath @@ -257,24 +257,24 @@ class Config: logger.verbose("Data path: %s" % self.dataPath) self.confFile = self.appHandle+".conf" - self.homePath = path.expanduser("~") + self.homePath = os.path.expanduser("~") self.lastPath = self.homePath - self.appPath = getattr(sys, "_MEIPASS", path.abspath(path.dirname(__file__))) - self.appRoot = path.join(self.appPath, path.pardir) - self.assetPath = path.join(self.appPath, "assets") - self.themeRoot = path.join(self.assetPath, "themes") - self.dictPath = path.join(self.assetPath, "dict") - self.iconPath = path.join(self.assetPath, "icons") - self.appIcon = path.join(self.iconPath, "novelwriter.svg") + self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) + self.appRoot = os.path.join(self.appPath, os.path.pardir) + self.assetPath = os.path.join(self.appPath, "assets") + self.themeRoot = os.path.join(self.assetPath, "themes") + self.dictPath = os.path.join(self.assetPath, "dict") + self.iconPath = os.path.join(self.assetPath, "icons") + self.appIcon = os.path.join(self.iconPath, "novelwriter.svg") logger.verbose("App path: %s" % self.appPath) logger.verbose("Home path: %s" % self.homePath) # If config folder does not exist, make it. # This assumes that the os config folder itself exists. - if not path.isdir(self.confPath): + if not os.path.isdir(self.confPath): try: - mkdir(self.confPath) + os.mkdir(self.confPath) except Exception as e: logger.error("Could not create folder: %s" % self.confPath) logger.error(str(e)) @@ -285,7 +285,7 @@ class Config: # Check if config file exists if self.confPath is not None: - if path.isfile(path.join(self.confPath, self.confFile)): + if os.path.isfile(os.path.join(self.confPath, self.confFile)): # If it exists, load it self.loadConfig() else: @@ -295,9 +295,9 @@ class Config: # If data folder does not exist, make it. # This assumes that the os data folder itself exists. if self.dataPath is not None: - if not path.isdir(self.dataPath): + if not os.path.isdir(self.dataPath): try: - mkdir(self.dataPath) + os.mkdir(self.dataPath) except Exception as e: logger.error("Could not create folder: %s" % self.dataPath) logger.error(str(e)) @@ -318,9 +318,9 @@ class Config: self.spellLanguage = "en" # Check if local help files exist - self.helpPath = path.join(self.assetPath, "help", "novelWriter.qhc") - self.hasHelp = path.isfile(self.helpPath) - self.hasHelp &= path.isfile(path.join(self.assetPath, "help", "novelWriter.qch")) + self.helpPath = os.path.join(self.assetPath, "help", "novelWriter.qhc") + self.hasHelp = os.path.isfile(self.helpPath) + self.hasHelp &= os.path.isfile(os.path.join(self.assetPath, "help", "novelWriter.qch")) logger.debug("Config initialisation complete") @@ -334,7 +334,7 @@ class Config: return False cnfParse = configparser.ConfigParser() - cnfPath = path.join(self.confPath, self.confFile) + cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="r", encoding="utf8") as inFile: cnfParse.read_file(inFile) @@ -629,7 +629,7 @@ class Config: cnfParse.set(cnfSec, "lastpath", str(self.lastPath)) # Write config file - cnfPath = path.join(self.confPath, self.confFile) + cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="w", encoding="utf8") as outFile: cnfParse.write(outFile) @@ -650,10 +650,10 @@ class Config: if self.dataPath is None: return False - cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE) + cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) self.recentProj = {} - if path.isfile(cacheFile): + if os.path.isfile(cacheFile): try: with open(cacheFile, mode="r", encoding="utf8") as inFile: theJson = inFile.read() @@ -690,8 +690,8 @@ class Config: if self.dataPath is None: return False - cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE) - cacheTemp = path.join(self.dataPath, nwFiles.RECENT_FILE+"~") + cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) + cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE+"~") try: with open(cacheTemp, mode="w+", encoding="utf8") as outFile: @@ -702,16 +702,16 @@ class Config: self.errData.append(str(e)) return False - if path.isfile(cacheFile): - unlink(cacheFile) - rename(cacheTemp, cacheFile) + if os.path.isfile(cacheFile): + os.unlink(cacheFile) + os.rename(cacheTemp, cacheFile) return True def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): """Add or update recent cache information o9n a given project. """ - self.recentProj[path.abspath(projPath)] = { + self.recentProj[os.path.abspath(projPath)] = { "title" : projTitle, "time" : int(saveTime), "words" : int(wordCount), @@ -737,27 +737,27 @@ class Config: def setConfPath(self, newPath): if newPath is None: return True - if not path.isfile(newPath): + if not os.path.isfile(newPath): logger.error("File not found, using default config path instead") return False - self.confPath = path.dirname(newPath) - self.confFile = path.basename(newPath) + self.confPath = os.path.dirname(newPath) + self.confFile = os.path.basename(newPath) return True def setDataPath(self, newPath): if newPath is None: return True - if not path.isdir(newPath): + if not os.path.isdir(newPath): logger.error("Path not found, using default data path instead") return False - self.dataPath = path.abspath(newPath) + self.dataPath = os.path.abspath(newPath) return True def setLastPath(self, lastPath): if lastPath is None or lastPath == "": self.lastPath = "" else: - self.lastPath = path.dirname(lastPath) + self.lastPath = os.path.dirname(lastPath) return True def setWinSize(self, newWidth, newHeight): diff --git a/nw/core/document.py b/nw/core/document.py index 4c0a5542..1261c12d 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -26,8 +26,7 @@ """ import logging - -from os import path, rename, unlink +import os from nw.constants import nwAlert from nw.common import isHandle @@ -89,12 +88,12 @@ class NWDoc(): docFile = self._docHandle+".nwd" logger.debug("Opening document %s" % docFile) - docPath = path.join(self.theProject.projContent, docFile) + docPath = os.path.join(self.theProject.projContent, docFile) self._fileLoc = docPath theText = "" self._docMeta = "" - if path.isfile(docPath): + if os.path.isfile(docPath): try: with open(docPath, mode="r", encoding="utf8") as inFile: fstLine = inFile.readline() @@ -137,8 +136,8 @@ class NWDoc(): docFile = self._docHandle+".nwd" logger.debug("Saving document %s" % docFile) - docPath = path.join(self.theProject.projContent, docFile) - docTemp = path.join(self.theProject.projContent, docFile+"~") + docPath = os.path.join(self.theProject.projContent, docFile) + docTemp = os.path.join(self.theProject.projContent, docFile+"~") if self._theItem is None: docMeta = "" @@ -163,9 +162,9 @@ class NWDoc(): # If we're here, the file was successfully saved, so we can # replace the temp file with the actual file - if path.isfile(docPath): - unlink(docPath) - rename(docTemp, docPath) + if os.path.isfile(docPath): + os.unlink(docPath) + os.rename(docTemp, docPath) self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName) @@ -181,13 +180,13 @@ class NWDoc(): docFile = tHandle+".nwd" chkList = [] - chkList.append(path.join(self.theProject.projContent, docFile)) - chkList.append(path.join(self.theProject.projContent, docFile+"~")) + chkList.append(os.path.join(self.theProject.projContent, docFile)) + chkList.append(os.path.join(self.theProject.projContent, docFile+"~")) for chkFile in chkList: - if path.isfile(chkFile): + if os.path.isfile(chkFile): try: - unlink(chkFile) + os.unlink(chkFile) logger.debug("Deleted: %s" % chkFile) except Exception as e: self.makeAlert(["Could not delete document file.", str(e)], nwAlert.ERROR) diff --git a/nw/core/index.py b/nw/core/index.py index c148abd7..06cddf60 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -28,8 +28,8 @@ import nw import logging import json +import os -from os import path from time import time from nw.constants import ( @@ -153,9 +153,9 @@ class NWIndex(): """Load index from last session from the project meta folder. """ theData = {} - indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) + indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) - if path.isfile(indexFile): + if os.path.isfile(indexFile): logger.debug("Loading index file") try: with open(indexFile, mode="r", encoding="utf8") as inFile: @@ -190,7 +190,7 @@ class NWIndex(): """Save the current index as a json file in the project meta data folder. """ - indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) + indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) logger.debug("Saving index file") if self.mainConf.debugInfo: diff --git a/nw/core/options.py b/nw/core/options.py index 7ffa2cdb..76d8621b 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -28,8 +28,7 @@ import logging import json - -from os import path +import os from nw.constants import nwFiles @@ -100,10 +99,10 @@ class OptionState(): if self.theProject.projMeta is None: return False - stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) + stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) theState = {} - if path.isfile(stateFile): + if os.path.isfile(stateFile): logger.debug("Loading GUI options file") try: with open(stateFile, mode="r", encoding="utf8") as inFile: @@ -130,7 +129,7 @@ class OptionState(): if self.theProject.projMeta is None: return False - stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) + stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) logger.debug("Saving GUI options file") try: diff --git a/nw/core/project.py b/nw/core/project.py index 72ac278d..cf8dd32f 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -27,8 +27,8 @@ import nw import logging +import os -from os import path, mkdir, listdir, unlink, rename, rmdir from lxml import etree from time import time from shutil import make_archive, unpack_archive, copyfile @@ -351,14 +351,11 @@ class NWProject(): aDoc.saveDocument("### %s\n\n" % scTitle) aDoc.clearDocument() - else: - # Fallback just in case. We shouldn't reach here. - self.newRoot("Novel", nwItemClass.NOVEL) - # Finalise - self.projOpened = time() - self.setProjectChanged(True) - self.saveProject(autoSave=True) + if popCustom or popMinimal: + self.projOpened = time() + self.setProjectChanged(True) + self.saveProject(autoSave=True) return True @@ -368,14 +365,14 @@ class NWProject(): parse the XML of the file and populate the project variables and build the tree of project items. """ - if not path.isfile(fileName): - fileName = path.join(fileName, nwFiles.PROJ_FILE) - if not path.isfile(fileName): + if not os.path.isfile(fileName): + fileName = os.path.join(fileName, nwFiles.PROJ_FILE) + if not os.path.isfile(fileName): self.makeAlert("File not found: %s" % fileName, nwAlert.ERROR) return False self.clearProject() - self.projPath = path.abspath(path.dirname(fileName)) + self.projPath = os.path.abspath(os.path.dirname(fileName)) logger.debug("Opening project: %s" % self.projPath) # Standard Folders and Files @@ -385,13 +382,13 @@ class NWProject(): self.clearProject() return False - self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) + self.projDict = os.path.join(self.projMeta, nwFiles.PROJ_DICT) # Check for Old Legacy Data # ========================= legacyList = [] # Cleanup is done later - for projItem in listdir(self.projPath): + for projItem in os.listdir(self.projPath): logger.verbose("Project contains: %s" % projItem) if projItem.startswith("data_"): legacyList.append(projItem) @@ -424,7 +421,7 @@ class NWProject(): # Trying to open backup file instead backFile = fileName[:-3]+"bak" - if path.isfile(backFile): + if os.path.isfile(backFile): self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO) try: nwXML = etree.parse(backFile) @@ -706,9 +703,9 @@ class NWProject(): self.projTree.packXML(nwXML) # Write the xml tree to file - tempFile = path.join(self.projPath, self.projFile+"~") - saveFile = path.join(self.projPath, self.projFile) - backFile = path.join(self.projPath, self.projFile[:-3]+"bak") + tempFile = os.path.join(self.projPath, self.projFile+"~") + saveFile = os.path.join(self.projPath, self.projFile) + backFile = os.path.join(self.projPath, self.projFile[:-3]+"bak") try: with open(tempFile, mode="wb") as outFile: outFile.write(etree.tostring( @@ -723,11 +720,11 @@ class NWProject(): # If we're here, the file was successfully saved, # so let's sort out the temps and backups - if path.isfile(backFile): - unlink(backFile) - if path.isfile(saveFile): - rename(saveFile, backFile) - rename(tempFile, saveFile) + if os.path.isfile(backFile): + os.unlink(backFile) + if os.path.isfile(saveFile): + os.rename(saveFile, backFile) + os.rename(tempFile, saveFile) # Save project GUI options self.optState.saveSettings() @@ -760,9 +757,9 @@ class NWProject(): if self.projPath is None or self.projPath == "": return False - self.projMeta = path.join(self.projPath, "meta") - self.projCache = path.join(self.projPath, "cache") - self.projContent = path.join(self.projPath, "content") + self.projMeta = os.path.join(self.projPath, "meta") + self.projCache = os.path.join(self.projPath, "cache") + self.projContent = os.path.join(self.projPath, "content") if not self._checkFolder(self.projMeta): return False @@ -797,7 +794,7 @@ class NWProject(): ), nwAlert.ERROR) return False - if not path.isdir(self.mainConf.backupPath): + if not os.path.isdir(self.mainConf.backupPath): self.theParent.makeAlert(( "Cannot backup project because the backup path does not exist. " "Please set a valid backup location in Tools > Preferences." @@ -805,10 +802,10 @@ class NWProject(): return False cleanName = makeFileNameSafe(self.projName) - baseDir = path.abspath(path.join(self.mainConf.backupPath, cleanName)) - if not path.isdir(baseDir): + baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName)) + if not os.path.isdir(baseDir): try: - mkdir(baseDir) + os.mkdir(baseDir) logger.debug("Created folder %s" % baseDir) except Exception as e: self.theParent.makeAlert( @@ -817,7 +814,7 @@ class NWProject(): ) return False - if path.commonpath([self.projPath, baseDir]) == self.projPath: + if os.path.commonpath([self.projPath, baseDir]) == self.projPath: self.theParent.makeAlert(( "Cannot backup project because the backup path is within the " "project folder to be backed up. Please choose a different " @@ -826,7 +823,7 @@ class NWProject(): return False archName = "Backup from %s" % formatTimeStamp(time(), fileSafe=True) - baseName = path.join(baseDir, archName) + baseName = os.path.join(baseDir, archName) try: self._clearLockFile() @@ -834,7 +831,7 @@ class NWProject(): self._writeLockFile() if doNotify: self.theParent.makeAlert( - "Backup archive file written to: %s.zip" % path.join(cleanName, archName), + "Backup archive file written to: %s.zip" % os.path.join(cleanName, archName), nwAlert.INFO ) else: @@ -861,11 +858,11 @@ class NWProject(): logger.error("No project path set for the example project") return False - srcSample = path.abspath(path.join(self.mainConf.appRoot, "sample")) - pkgSample = path.join(self.mainConf.assetPath, "sample.zip") + srcSample = os.path.abspath(os.path.join(self.mainConf.appRoot, "sample")) + pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip") isSuccess = False - if path.isfile(pkgSample): + if os.path.isfile(pkgSample): self.setProjectPath(projPath, newProject=True) try: @@ -876,19 +873,19 @@ class NWProject(): ["Failed to create a new example project.", str(e)], nwAlert.ERROR ) - elif path.isdir(srcSample): + elif os.path.isdir(srcSample): self.setProjectPath(projPath, newProject=True) try: - srcProj = path.join(srcSample, nwFiles.PROJ_FILE) - dstProj = path.join(projPath, nwFiles.PROJ_FILE) + srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE) + dstProj = os.path.join(projPath, nwFiles.PROJ_FILE) copyfile(srcProj, dstProj) - srcContent = path.join(srcSample, "content") - dstContent = path.join(projPath, "content") - for srcFile in listdir(srcContent): - srcDoc = path.join(srcContent, srcFile) - dstDoc = path.join(dstContent, srcFile) + srcContent = os.path.join(srcSample, "content") + dstContent = os.path.join(projPath, "content") + for srcFile in os.listdir(srcContent): + srcDoc = os.path.join(srcContent, srcFile) + dstDoc = os.path.join(dstContent, srcFile) copyfile(srcDoc, dstDoc) isSuccess = True @@ -923,13 +920,13 @@ class NWProject(): self.projPath = None else: if projPath.startswith("~"): - projPath = path.expanduser(projPath) - self.projPath = path.abspath(projPath) + projPath = os.path.expanduser(projPath) + self.projPath = os.path.abspath(projPath) if newProject: - if not path.isdir(projPath): + if not os.path.isdir(projPath): try: - mkdir(projPath) + os.mkdir(projPath) logger.debug("Created folder %s" % projPath) except Exception as e: self.theParent.makeAlert(( @@ -937,8 +934,8 @@ class NWProject(): ), nwAlert.ERROR) return False - if path.isdir(projPath): - if listdir(self.projPath): + if os.path.isdir(projPath): + if os.listdir(self.projPath): self.theParent.makeAlert(( "New project folder is not empty. " "Each project requires a dedicated project folder." @@ -988,7 +985,7 @@ class NWProject(): """ self.doBackup = doBackup if doBackup: - if not path.isdir(self.mainConf.backupPath): + if not os.path.isdir(self.mainConf.backupPath): self.theParent.makeAlert(( "You must set a valid backup path in preferences to use " "the automatic project backup feature." @@ -1187,8 +1184,8 @@ class NWProject(): if self.projPath is None: return ["ERROR"] - lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK) - if not path.isfile(lockFile): + lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK) + if not os.path.isfile(lockFile): return [] try: @@ -1213,7 +1210,7 @@ class NWProject(): if self.projPath is None: return False - lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK) + lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK) try: with open(lockFile, mode="w+", encoding="utf8") as outFile: outFile.write("%s\n" % self.mainConf.hostName) @@ -1234,10 +1231,10 @@ class NWProject(): if self.projPath is None: return False - lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK) - if path.isfile(lockFile): + lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK) + if os.path.isfile(lockFile): try: - unlink(lockFile) + os.unlink(lockFile) return True except Exception as e: logger.error("Failed to remove project lockfile") @@ -1249,9 +1246,9 @@ class NWProject(): def _checkFolder(self, thePath): """Check if a folder exists, and if it doesn't, create it. """ - if not path.isdir(thePath): + if not os.path.isdir(thePath): try: - mkdir(thePath) + os.mkdir(thePath) logger.debug("Created folder %s" % thePath) except Exception as e: self.makeAlert(["Could not create folder.", str(e)], nwAlert.ERROR) @@ -1292,7 +1289,7 @@ class NWProject(): # Then check the files in the data folder logger.debug("Checking files in project content folder") orphanFiles = [] - for fileItem in listdir(self.projContent): + for fileItem in os.listdir(self.projContent): if not fileItem.endswith(".nwd"): logger.warning("Skipping file %s" % fileItem) continue @@ -1355,8 +1352,8 @@ class NWProject(): if not self.ensureFolderStructure(): return False - sessionFile = path.join(self.projMeta, nwFiles.SESS_STATS) - isFile = path.isfile(sessionFile) + sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS) + isFile = os.path.isfile(sessionFile) with open(sessionFile, mode="a+", encoding="utf8") as outFile: if not isFile: @@ -1383,8 +1380,8 @@ class NWProject(): def _legacyDataFolder(self, theFolder, errList): """Clean up legacy data folders. """ - theData = path.join(self.projPath, theFolder) - if not path.isdir(theData): + theData = os.path.join(self.projPath, theFolder) + if not os.path.isdir(theData): errList.append("Not a folder: %s" % theData) return errList @@ -1392,9 +1389,9 @@ class NWProject(): # Move Documents to Content # ========================= - for dataItem in listdir(theData): - theFile = path.join(theData, dataItem) - if not path.isfile(theFile): + for dataItem in os.listdir(theData): + theFile = os.path.join(theData, dataItem) + if not os.path.isfile(theFile): theErr = self._moveUnknownItem(theData, dataItem) if theErr: errList.append(theErr) @@ -1402,9 +1399,9 @@ class NWProject(): if len(dataItem) == 21 and dataItem.endswith("_main.nwd"): tHandle = theFolder[-1]+dataItem[:12] - newPath = path.join(self.projContent, tHandle+".nwd") + newPath = os.path.join(self.projContent, tHandle+".nwd") try: - rename(theFile, newPath) + os.rename(theFile, newPath) logger.info("Moved file: %s" % theFile) logger.info("New location: %s" % newPath) except Exception as e: @@ -1413,7 +1410,7 @@ class NWProject(): elif len(dataItem) == 21 and dataItem.endswith("_main.bak"): try: - unlink(theFile) + os.unlink(theFile) logger.info("Deleted file: %s" % theFile) except Exception as e: logger.error(str(e)) @@ -1427,7 +1424,7 @@ class NWProject(): # Remove Data Folder # ================== try: - rmdir(theData) + os.rmdir(theData) logger.info("Removed folder: %s" % theFolder) except Exception as e: logger.error(str(e)) @@ -1439,15 +1436,15 @@ class NWProject(): """Move an item that doesn't belong in the project folder to a junk folder. """ - theJunk = path.join(self.projPath, "junk") + theJunk = os.path.join(self.projPath, "junk") if not self._checkFolder(theJunk): return "Could not make folder: %s" % theJunk - theSrc = path.join(theDir, theItem) - theDst = path.join(theJunk, theItem) + theSrc = os.path.join(theDir, theItem) + theDst = os.path.join(theJunk, theItem) try: - rename(theSrc, theDst) + os.rename(theSrc, theDst) logger.info("Moved to junk: %s" % theSrc) except Exception as e: logger.error(str(e)) @@ -1459,29 +1456,29 @@ class NWProject(): """Delete files that are no longer used by novelWriter. """ rmList = [ - path.join(self.projCache, "nwProject.nwx.0"), - path.join(self.projCache, "nwProject.nwx.1"), - path.join(self.projCache, "nwProject.nwx.2"), - path.join(self.projCache, "nwProject.nwx.3"), - path.join(self.projCache, "nwProject.nwx.4"), - path.join(self.projCache, "nwProject.nwx.5"), - path.join(self.projCache, "nwProject.nwx.6"), - path.join(self.projCache, "nwProject.nwx.7"), - path.join(self.projCache, "nwProject.nwx.8"), - path.join(self.projCache, "nwProject.nwx.9"), - path.join(self.projMeta, "mainOptions.json"), - path.join(self.projMeta, "exportOptions.json"), - path.join(self.projMeta, "outlineOptions.json"), - path.join(self.projMeta, "timelineOptions.json"), - path.join(self.projMeta, "docMergeOptions.json"), - path.join(self.projMeta, "sessionLogOptions.json"), + os.path.join(self.projCache, "nwProject.nwx.0"), + os.path.join(self.projCache, "nwProject.nwx.1"), + os.path.join(self.projCache, "nwProject.nwx.2"), + os.path.join(self.projCache, "nwProject.nwx.3"), + os.path.join(self.projCache, "nwProject.nwx.4"), + os.path.join(self.projCache, "nwProject.nwx.5"), + os.path.join(self.projCache, "nwProject.nwx.6"), + os.path.join(self.projCache, "nwProject.nwx.7"), + os.path.join(self.projCache, "nwProject.nwx.8"), + os.path.join(self.projCache, "nwProject.nwx.9"), + os.path.join(self.projMeta, "mainOptions.json"), + os.path.join(self.projMeta, "exportOptions.json"), + os.path.join(self.projMeta, "outlineOptions.json"), + os.path.join(self.projMeta, "timelineOptions.json"), + os.path.join(self.projMeta, "docMergeOptions.json"), + os.path.join(self.projMeta, "sessionLogOptions.json"), ] for rmFile in rmList: - if path.isfile(rmFile): + if os.path.isfile(rmFile): logger.info("Deleting: %s" % rmFile) try: - unlink(rmFile) + os.unlink(rmFile) except Exception as e: logger.error(str(e)) diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index b51a9a63..02d0f1b3 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -27,8 +27,8 @@ import nw import logging +import os -from os import path, listdir from difflib import get_close_matches from nw.constants import isoLanguage @@ -108,7 +108,7 @@ class NWSpellCheck(): self.PROJW = [] if projectDict is not None: self.projectDict = projectDict - if not path.isfile(projectDict): + if not os.path.isfile(projectDict): return try: logger.debug("Loading project word list") @@ -228,7 +228,7 @@ class NWSpellSimple(NWSpellCheck): """Load a dictionary as a list from the app assets folder. """ self.WORDS = [] - dictFile = path.join(self.mainConf.dictPath, theLang+".dict") + dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict") try: with open(dictFile, mode="r", encoding="utf-8") as wordsFile: for theLine in wordsFile: @@ -297,9 +297,9 @@ class NWSpellSimple(NWSpellCheck): """Lists the dictionary files in the app assets folder. """ retList = [] - for dictFile in listdir(self.mainConf.dictPath): + for dictFile in os.listdir(self.mainConf.dictPath): - theBits = path.splitext(dictFile) + theBits = os.path.splitext(dictFile) if len(theBits) != 2: continue if theBits[1] != ".dict": diff --git a/nw/core/tree.py b/nw/core/tree.py index c7939c3b..9389d2db 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -27,8 +27,8 @@ import logging import json +import os -from os import path from lxml import etree from hashlib import sha256 from time import time @@ -144,8 +144,8 @@ class NWTree(): the project directory. These files are there to assist the user if they wish to browse the stored files. """ - tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT) - tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON) + tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT) + tocJson = os.path.join(self.theProject.projPath, nwFiles.TOC_JSON) jsonData = [] try: @@ -162,14 +162,14 @@ class NWTree(): if tItem is None: continue tFile = tHandle+".nwd" - if path.isfile(path.join(self.theProject.projContent, tFile)): + if os.path.isfile(os.path.join(self.theProject.projContent, tFile)): outFile.write(" %-25s %-9s %s\n" % ( - path.join("content", tFile), + os.path.join("content", tFile), tItem.itemClass.name, tItem.itemName, )) jsonData.append([ - path.join("content", tFile), + os.path.join("content", tFile), tItem.itemClass.name, tItem.itemName, ]) diff --git a/nw/gui/about.py b/nw/gui/about.py index add97c7c..c6957bae 100644 --- a/nw/gui/about.py +++ b/nw/gui/about.py @@ -27,8 +27,8 @@ import nw import logging +import os -from os import path from datetime import datetime from PyQt5.QtCore import Qt @@ -197,8 +197,8 @@ class GuiAbout(QDialog): """Load the content for the License page. """ docName = "gplv3_%s.htm" % self.mainConf.guiLang - docPath = path.join(self.mainConf.assetPath, "text", docName) - if path.isfile(docPath): + docPath = os.path.join(self.mainConf.assetPath, "text", docName) + if os.path.isfile(docPath): with open(docPath, mode="r", encoding="utf8") as inFile: helpText = inFile.read() self.pageLicense.setHtml(helpText) diff --git a/nw/gui/build.py b/nw/gui/build.py index f2c15221..cbad4f84 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -28,8 +28,8 @@ import nw import logging import json +import os -from os import path from time import time from datetime import datetime @@ -630,8 +630,8 @@ class GuiBuildNovel(QDialog): cleanName = makeFileNameSafe(self.theProject.projName) fileName = "%s.%s" % (cleanName, fileExt) saveDir = self.mainConf.lastPath - savePath = path.join(saveDir, fileName) - if not path.isdir(saveDir): + savePath = os.path.join(saveDir, fileName) + if not os.path.isdir(saveDir): saveDir = self.mainConf.homePath if self.mainConf.showGUI: @@ -792,9 +792,9 @@ class GuiBuildNovel(QDialog): def _loadCache(self): """Save the current data to cache. """ - buildCache = path.join(self.theProject.projCache, nwFiles.BUILD_CACHE) + buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE) dataCount = 0 - if path.isfile(buildCache): + if os.path.isfile(buildCache): logger.debug("Loading build cache") try: @@ -823,7 +823,7 @@ class GuiBuildNovel(QDialog): def _saveCache(self): """Save the current data to cache. """ - buildCache = path.join(self.theProject.projCache, nwFiles.BUILD_CACHE) + buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE) if self.mainConf.debugInfo: nIndent = 2 diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 5cbfbc72..ea475b70 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -27,8 +27,7 @@ import nw import logging - -from os import path +import os from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont @@ -335,7 +334,7 @@ class GuiConfigEditGeneralTab(QWidget): """Open a dialog to select the backup folder. """ currDir = self.backupPath - if not path.isdir(currDir): + if not os.path.isdir(currDir): currDir = "" dlgOpt = QFileDialog.Options() diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 200d8a93..38356093 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -27,8 +27,8 @@ import nw import logging +import os -from os import path from datetime import datetime from PyQt5.QtCore import Qt, QSize @@ -186,7 +186,7 @@ class GuiProjectLoad(QDialog): options=dlgOpt ) if projFile: - thePath = path.abspath(path.dirname(projFile)) + thePath = os.path.abspath(os.path.dirname(projFile)) self.selPath.setText(thePath) self.openPath = thePath self.openState = self.OPEN_STATE diff --git a/nw/gui/projwizard.py b/nw/gui/projwizard.py index 475ec907..1beb4e54 100644 --- a/nw/gui/projwizard.py +++ b/nw/gui/projwizard.py @@ -27,8 +27,7 @@ import nw import logging - -from os import path +import os from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( @@ -208,7 +207,7 @@ class ProjWizardFolderPage(QWizardPage): """Select a project folder. """ lastPath = self.mainConf.lastPath - if not path.isdir(lastPath): + if not os.path.isdir(lastPath): lastPath = "" dlgOpt = QFileDialog.Options() @@ -220,7 +219,7 @@ class ProjWizardFolderPage(QWizardPage): if projDir: projName = self.field("projName") if projName is not None: - fullDir = path.join(path.abspath(projDir), makeFileNameSafe(projName)) + fullDir = os.path.join(os.path.abspath(projDir), makeFileNameSafe(projName)) self.projPath.setText(fullDir) else: self.projPath.setText("") diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 3c67fb8c..3a511907 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -29,8 +29,8 @@ import nw import logging import configparser +import os -from os import path, listdir from math import ceil from PyQt5.QtCore import Qt @@ -182,21 +182,21 @@ class GuiTheme: """Add the fonts in the assets fonts folder to the app. """ ttfList = [] - fontAssets = path.join(self.mainConf.assetPath, self.fontPath) - for fontFam in listdir(fontAssets): - fontDir = path.join(fontAssets, fontFam) - if path.isdir(fontDir): + fontAssets = os.path.join(self.mainConf.assetPath, self.fontPath) + for fontFam in os.listdir(fontAssets): + fontDir = os.path.join(fontAssets, fontFam) + if os.path.isdir(fontDir): if fontFam not in self.guiFontDB.families(): - for fontFile in listdir(fontDir): - ttfFile = path.join(fontDir, fontFile) - if path.isfile(ttfFile) and fontFile.endswith(".ttf"): + for fontFile in os.listdir(fontDir): + ttfFile = os.path.join(fontDir, fontFile) + if os.path.isfile(ttfFile) and fontFile.endswith(".ttf"): ttfList.append(ttfFile) for ttfFile in ttfList: - logger.verbose("Font asset: %s" % path.relpath(ttfFile)) + logger.verbose("Font asset: %s" % os.path.relpath(ttfFile)) fontID = self.guiFontDB.addApplicationFont(ttfFile) if fontID < 0: - logger.error("Failed to add font: %s" % path.relpath(ttfFile)) + logger.error("Failed to add font: %s" % os.path.relpath(ttfFile)) return @@ -227,10 +227,10 @@ class GuiTheme: self.guiTheme = self.mainConf.guiTheme self.guiSyntax = self.mainConf.guiSyntax self.themeRoot = self.mainConf.themeRoot - self.themePath = path.join(self.mainConf.themeRoot, self.guiPath, self.guiTheme) - self.syntaxFile = path.join(self.themeRoot, self.syntaxPath, self.guiSyntax+".conf") - self.confFile = path.join(self.themePath, self.confName) - self.cssFile = path.join(self.themePath, self.cssName) + self.themePath = os.path.join(self.mainConf.themeRoot, self.guiPath, self.guiTheme) + self.syntaxFile = os.path.join(self.themeRoot, self.syntaxPath, self.guiSyntax+".conf") + self.confFile = os.path.join(self.themePath, self.confName) + self.cssFile = os.path.join(self.themePath, self.cssName) self.loadTheme() self.loadSyntax() @@ -259,7 +259,7 @@ class GuiTheme: # CSS File cssData = "" try: - if path.isfile(self.cssFile): + if os.path.isfile(self.cssFile): with open(self.cssFile, mode="r", encoding="utf8") as inFile: cssData = inFile.read() except Exception as e: @@ -375,8 +375,8 @@ class GuiTheme: return self.themeList confParser = configparser.ConfigParser() - for themeDir in listdir(path.join(self.mainConf.themeRoot, self.guiPath)): - themeConf = path.join(self.mainConf.themeRoot, self.guiPath, themeDir, self.confName) + for themeDir in os.listdir(os.path.join(self.mainConf.themeRoot, self.guiPath)): + themeConf = os.path.join(self.mainConf.themeRoot, self.guiPath, themeDir, self.confName) logger.verbose("Checking theme config for '%s'" % themeDir) try: with open(themeConf, mode="r", encoding="utf8") as inFile: @@ -405,10 +405,10 @@ class GuiTheme: return self.syntaxList confParser = configparser.ConfigParser() - syntaxDir = path.join(self.mainConf.themeRoot, self.syntaxPath) - for syntaxFile in listdir(syntaxDir): - syntaxPath = path.join(syntaxDir, syntaxFile) - if not path.isfile(syntaxPath): + syntaxDir = os.path.join(self.mainConf.themeRoot, self.syntaxPath) + for syntaxFile in os.listdir(syntaxDir): + syntaxPath = os.path.join(syntaxDir, syntaxFile) + if not os.path.isfile(syntaxPath): continue logger.verbose("Checking theme syntax for '%s'" % syntaxFile) try: @@ -612,11 +612,11 @@ class GuiIcons: logger.debug("Loading icon theme files") self.themeMap = {} - checkPath = path.join(self.mainConf.iconPath, self.mainConf.guiIcons) - if path.isdir(checkPath): + checkPath = os.path.join(self.mainConf.iconPath, self.mainConf.guiIcons) + if os.path.isdir(checkPath): logger.debug("Loading icon theme '%s'" % self.mainConf.guiIcons) self.iconPath = checkPath - self.confFile = path.join(checkPath, self.confName) + self.confFile = os.path.join(checkPath, self.confName) else: return False @@ -648,8 +648,8 @@ class GuiIcons: if iconName not in self.ICON_MAP: logger.error("Unknown icon name '%s' in config file" % iconName) else: - iconPath = path.join(self.iconPath, iconFile) - if path.isfile(iconPath): + iconPath = os.path.join(self.iconPath, iconFile) + if os.path.isfile(iconPath): self.themeMap[iconName] = iconPath logger.verbose("Icon slot '%s' using file '%s'" % (iconName, iconFile)) else: @@ -671,10 +671,10 @@ class GuiIcons: logger.error("Decoration with name '%s' does not exist" % decoKey) return QPixmap() - imgPath = path.join( + imgPath = os.path.join( self.mainConf.assetPath, "images", self.DECO_MAP[decoKey] ) - if not path.isfile(imgPath): + if not os.path.isfile(imgPath): logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey]) return QPixmap() @@ -714,11 +714,11 @@ class GuiIcons: return self.themeList confParser = configparser.ConfigParser() - for themeDir in listdir(self.mainConf.iconPath): - themePath = path.join(self.mainConf.iconPath, themeDir) - if not path.isdir(themePath) or themeDir == self.fbackName: + for themeDir in os.listdir(self.mainConf.iconPath): + themePath = os.path.join(self.mainConf.iconPath, themeDir) + if not os.path.isdir(themePath) or themeDir == self.fbackName: continue - themeConf = path.join(themePath, self.confName) + themeConf = os.path.join(themePath, self.confName) logger.verbose("Checking icon theme config for '%s'" % themeDir) try: with open(themeConf, mode="r", encoding="utf8") as inFile: @@ -756,12 +756,12 @@ class GuiIcons: # If we just want the app icon, return it right away if iconKey == "novelwriter": - return QIcon(path.join(self.mainConf.iconPath, "novelwriter.svg")) + return QIcon(os.path.join(self.mainConf.iconPath, "novelwriter.svg")) # Otherwise, we start looking for it # First in the theme folder if iconKey in self.themeMap: - logger.verbose("Loading: %s" % path.relpath(self.themeMap[iconKey])) + logger.verbose("Loading: %s" % os.path.relpath(self.themeMap[iconKey])) return QIcon(self.themeMap[iconKey]) # Next, we try to load the Qt style icons @@ -777,14 +777,14 @@ class GuiIcons: # Finally. we check if we have a fallback icon if self.mainConf.guiDark: - fbackIcon = path.join( + fbackIcon = os.path.join( self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey ) - if path.isfile(fbackIcon): + if os.path.isfile(fbackIcon): logger.verbose("Loading icon '%s' from fallback theme (dark mode)" % iconKey) return QIcon(fbackIcon) - fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey) - if path.isfile(fbackIcon): + fbackIcon = os.path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey) + if os.path.isfile(fbackIcon): logger.verbose("Loading icon '%s' from fallback theme (light mode)" % iconKey) return QIcon(fbackIcon) diff --git a/nw/gui/writingstats.py b/nw/gui/writingstats.py index 4c27520e..d45b3726 100644 --- a/nw/gui/writingstats.py +++ b/nw/gui/writingstats.py @@ -28,8 +28,8 @@ import nw import logging import json +import os -from os import path from datetime import datetime from PyQt5.QtCore import Qt @@ -322,8 +322,8 @@ class GuiWritingStats(QDialog): if fileExt: fileName = "sessionStats.%s" % fileExt saveDir = self.mainConf.lastPath - savePath = path.join(saveDir, fileName) - if not path.isdir(saveDir): + savePath = os.path.join(saveDir, fileName) + if not os.path.isdir(saveDir): saveDir = self.mainConf.homePath dlgOpt = QFileDialog.Options() @@ -410,7 +410,7 @@ class GuiWritingStats(QDialog): ttTime = 0 try: - logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) + logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS) with open(logFile, mode="r", encoding="utf8") as inFile: for inLine in inFile: if inLine.startswith("#"): diff --git a/nw/guimain.py b/nw/guimain.py index 44b99d56..bfb02bd5 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -27,8 +27,8 @@ import nw import logging +import os -from os import path from datetime import datetime from time import time @@ -268,7 +268,7 @@ class GuiMain(QMainWindow): logger.error("No projData or projPath set") return False - if path.isfile(path.join(projPath, self.theProject.projFile)): + if os.path.isfile(os.path.join(projPath, self.theProject.projFile)): self.makeAlert( "A project already exists in that location. Please choose another folder.", nwAlert.ERROR diff --git a/setup.py b/setup.py index 1ae7d6b7..e3ce209c 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import os import sys -import shutil import subprocess import setuptools @@ -42,7 +41,7 @@ if buildDocs: buildFail = False try: - subprocess.call(["make","-C", "docs", "qthelp"]) + subprocess.call(["make", "-C", "docs", "qthelp"]) except Exception as e: print("Failed with error:") print(str(e)) @@ -98,7 +97,7 @@ if buildSample: from zipfile import ZipFile with ZipFile(dstSample, "w") as zipObj: - zipObj.write(os.path.join("sample", "nwProject.nwx"), "nwProject.nwx") + zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") for docFile in os.listdir(os.path.join(srcSample, "content")): srcDoc = os.path.join(srcSample, "content", docFile) zipObj.write(srcDoc, "content/"+docFile) diff --git a/tests/conftest.py b/tests/conftest.py index 8967ca5d..b8cc2fef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,13 +5,13 @@ import sys import pytest import shutil +import os -from os import path, mkdir from nwdummy import DummyMain from PyQt5.QtWidgets import QMessageBox -sys.path.insert(1, path.abspath(path.join(path.dirname(__file__), path.pardir))) +sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) from nw.config import Config # noqa: E402 @@ -25,12 +25,12 @@ def nwTemp(): presistent after the test so that the status of generated files can be checked. The folder is instead cleared before a new test session. """ - testDir = path.dirname(__file__) - tempDir = path.join(testDir, "temp") - if path.isdir(tempDir): + testDir = os.path.dirname(__file__) + tempDir = os.path.join(testDir, "temp") + if os.path.isdir(tempDir): shutil.rmtree(tempDir) - if not path.isdir(tempDir): - mkdir(tempDir) + if not os.path.isdir(tempDir): + os.mkdir(tempDir) return tempDir @pytest.fixture(scope="session") @@ -38,8 +38,8 @@ def nwRef(): """The folder where all the reference files are stored for verifying the results of tests. """ - testDir = path.dirname(__file__) - refDir = path.join(testDir, "reference") + testDir = os.path.dirname(__file__) + refDir = os.path.join(testDir, "reference") return refDir ## @@ -80,40 +80,40 @@ def nwDummy(nwRef, nwTemp, nwConf): def nwTempProj(nwTemp): """A temporary folder for project tests. """ - projDir = path.join(nwTemp, "proj") - if not path.isdir(projDir): - mkdir(projDir) + projDir = os.path.join(nwTemp, "proj") + if not os.path.isdir(projDir): + os.mkdir(projDir) return projDir @pytest.fixture(scope="session") def nwTempGUI(nwTemp): """A temporary folder for GUI tests. """ - guiDir = path.join(nwTemp, "gui") - if not path.isdir(guiDir): - mkdir(guiDir) + guiDir = os.path.join(nwTemp, "gui") + if not os.path.isdir(guiDir): + os.mkdir(guiDir) return guiDir @pytest.fixture(scope="session") def nwTempBuild(nwTemp): """A temporary folder for build tests. """ - buildDir = path.join(nwTemp, "build") - if not path.isdir(buildDir): - mkdir(buildDir) + buildDir = os.path.join(nwTemp, "build") + if not os.path.isdir(buildDir): + os.mkdir(buildDir) return buildDir @pytest.fixture(scope="function") def nwFuncTemp(nwTemp): """A temporary folder for a single test function. """ - funcDir = path.join(nwTemp, "ftemp") - if path.isdir(funcDir): + funcDir = os.path.join(nwTemp, "ftemp") + if os.path.isdir(funcDir): shutil.rmtree(funcDir) - if not path.isdir(funcDir): - mkdir(funcDir) + if not os.path.isdir(funcDir): + os.mkdir(funcDir) yield funcDir - if path.isdir(funcDir): + if os.path.isdir(funcDir): shutil.rmtree(funcDir) return @@ -125,20 +125,20 @@ def nwFuncTemp(nwTemp): def nwMinimal(nwTemp): """A minimal novelWriter example project. """ - testDir = path.dirname(__file__) - minimalStore = path.join(testDir, "minimal") - minimalDir = path.join(nwTemp, "minimal") - if path.isdir(minimalDir): + testDir = os.path.dirname(__file__) + minimalStore = os.path.join(testDir, "minimal") + minimalDir = os.path.join(nwTemp, "minimal") + if os.path.isdir(minimalDir): shutil.rmtree(minimalDir) shutil.copytree(minimalStore, minimalDir) - cacheDir = path.join(minimalDir, "cache") - if path.isdir(cacheDir): + cacheDir = os.path.join(minimalDir, "cache") + if os.path.isdir(cacheDir): shutil.rmtree(cacheDir) - metaDir = path.join(minimalDir, "meta") - if path.isdir(metaDir): + metaDir = os.path.join(minimalDir, "meta") + if os.path.isdir(metaDir): shutil.rmtree(metaDir) yield minimalDir - if path.isdir(minimalDir): + if os.path.isdir(minimalDir): shutil.rmtree(minimalDir) return @@ -147,20 +147,20 @@ def nwLipsum(nwTemp): """A medium sized novelWriter example project with a lot of Lorem Ipsum dummy text. """ - testDir = path.dirname(__file__) - lipsumStore = path.join(testDir, "lipsum") - lipsumDir = path.join(nwTemp, "lipsum") - if path.isdir(lipsumDir): + testDir = os.path.dirname(__file__) + lipsumStore = os.path.join(testDir, "lipsum") + lipsumDir = os.path.join(nwTemp, "lipsum") + if os.path.isdir(lipsumDir): shutil.rmtree(lipsumDir) shutil.copytree(lipsumStore, lipsumDir) - cacheDir = path.join(lipsumDir, "cache") - if path.isdir(cacheDir): + cacheDir = os.path.join(lipsumDir, "cache") + if os.path.isdir(cacheDir): shutil.rmtree(cacheDir) - metaDir = path.join(lipsumDir, "meta") - if path.isdir(metaDir): + metaDir = os.path.join(lipsumDir, "meta") + if os.path.isdir(metaDir): shutil.rmtree(metaDir) yield lipsumDir - if path.isdir(lipsumDir): + if os.path.isdir(lipsumDir): shutil.rmtree(lipsumDir) return @@ -168,14 +168,14 @@ def nwLipsum(nwTemp): def nwOldProj(nwTemp): """A minimal movelWriter project using the old folder structure. """ - testDir = path.dirname(__file__) - oldProjStore = path.join(testDir, "oldproj") - oldProjDir = path.join(nwTemp, "oldproj") - if path.isdir(oldProjDir): + testDir = os.path.dirname(__file__) + oldProjStore = os.path.join(testDir, "oldproj") + oldProjDir = os.path.join(nwTemp, "oldproj") + if os.path.isdir(oldProjDir): shutil.rmtree(oldProjDir) shutil.copytree(oldProjStore, oldProjDir) yield oldProjDir - if path.isdir(oldProjDir): + if os.path.isdir(oldProjDir): shutil.rmtree(oldProjDir) return diff --git a/tests/profilestats.py b/tests/profilestats.py index f130d93c..550aba00 100755 --- a/tests/profilestats.py +++ b/tests/profilestats.py @@ -2,15 +2,14 @@ # -*- coding: utf-8 -*- import pstats +import os -from os import path - -profDir = path.abspath(path.join(path.dirname(__file__), path.pardir, "prof")) +profDir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, "prof")) print("") print("Profiles directory: %s" % profDir) print("") -profMainWindows = pstats.Stats(path.join(profDir, "testMainWindows.prof")) +profMainWindows = pstats.Stats(os.path.join(profDir, "testMainWindows.prof")) profMainWindows.sort_stats("cumtime") profMainWindows.print_stats("nw/") diff --git a/tests/test_config.py b/tests/test_config.py index 612352cf..8911a69d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,13 +3,14 @@ """ import pytest +import os + from nwtools import cmpFiles -from os import path @pytest.mark.core def testConfigCore(tmpConf, nwTemp, nwRef): - refConf = path.join(nwRef, "novelwriter.conf") - testConf = path.join(tmpConf.confPath, "novelwriter.conf") + refConf = os.path.join(nwRef, "novelwriter.conf") + testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") assert tmpConf.confPath == nwTemp assert tmpConf.saveConfig() @@ -22,8 +23,8 @@ def testConfigCore(tmpConf, nwTemp, nwRef): @pytest.mark.core def testConfigSetConfPath(tmpConf, nwTemp): assert tmpConf.setConfPath(None) - assert not tmpConf.setConfPath(path.join("somewhere", "over", "the", "rainbow")) - assert tmpConf.setConfPath(path.join(nwTemp, "novelwriter.conf")) + assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow")) + assert tmpConf.setConfPath(os.path.join(nwTemp, "novelwriter.conf")) assert tmpConf.confPath == nwTemp assert tmpConf.confFile == "novelwriter.conf" assert not tmpConf.confChanged @@ -31,15 +32,15 @@ def testConfigSetConfPath(tmpConf, nwTemp): @pytest.mark.core def testConfigSetDataPath(tmpConf, nwTemp): assert tmpConf.setDataPath(None) - assert not tmpConf.setDataPath(path.join("somewhere", "over", "the", "rainbow")) + assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow")) assert tmpConf.setDataPath(nwTemp) assert tmpConf.dataPath == nwTemp assert not tmpConf.confChanged @pytest.mark.core def testConfigSetWinSize(tmpConf, nwTemp, nwRef): - refConf = path.join(nwRef, "novelwriter.conf") - testConf = path.join(tmpConf.confPath, "novelwriter.conf") + refConf = os.path.join(nwRef, "novelwriter.conf") + testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") tmpConf.guiScale = 1.0 assert tmpConf.confPath == nwTemp @@ -55,8 +56,8 @@ def testConfigSetWinSize(tmpConf, nwTemp, nwRef): @pytest.mark.core def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef): - refConf = path.join(nwRef, "novelwriter.conf") - testConf = path.join(tmpConf.confPath, "novelwriter.conf") + refConf = os.path.join(nwRef, "novelwriter.conf") + testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") assert tmpConf.confPath == nwTemp tmpConf.guiScale = 1.0 @@ -77,8 +78,8 @@ def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef): @pytest.mark.core def testConfigSetPanePos(tmpConf, nwTemp, nwRef): - refConf = path.join(nwRef, "novelwriter.conf") - testConf = path.join(tmpConf.confPath, "novelwriter.conf") + refConf = os.path.join(nwRef, "novelwriter.conf") + testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") assert tmpConf.confPath == nwTemp @@ -113,8 +114,8 @@ def testConfigSetPanePos(tmpConf, nwTemp, nwRef): @pytest.mark.core def testConfigFlags(tmpConf, nwTemp, nwRef): - refConf = path.join(nwRef, "novelwriter.conf") - testConf = path.join(tmpConf.confPath, "novelwriter.conf") + refConf = os.path.join(nwRef, "novelwriter.conf") + testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") assert tmpConf.confPath == nwTemp @@ -150,7 +151,7 @@ def testTextSizes(tmpConf, nwTemp, nwRef): @pytest.mark.core def testConfigErrors(tmpConf): - nonPath = path.join("somewhere", "over", "the", "rainbow") + nonPath = os.path.join("somewhere", "over", "the", "rainbow") assert tmpConf.initConfig(nonPath, nonPath) assert tmpConf.hasError assert not tmpConf.loadConfig() diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 3de991dc..a95eba02 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -5,12 +5,11 @@ import nw import pytest import json +import os from shutil import copyfile from nwtools import cmpFiles, getGuiItem -from os import path - from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtWidgets import ( QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog, QAction, @@ -130,9 +129,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR qtbot.wait(stepDelay) # Check the files - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempGUI, "2_nwProject.nwx") - refFile = path.join(nwRef, "gui", "2_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempGUI, "2_nwProject.nwx") + refFile = os.path.join(nwRef, "gui", "2_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 8, 9, 10]) @@ -189,9 +188,9 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): qtbot.wait(stepDelay) # Check the files - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempGUI, "3_nwProject.nwx") - refFile = path.join(nwRef, "gui", "3_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempGUI, "3_nwProject.nwx") + refFile = os.path.join(nwRef, "gui", "3_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) @@ -273,7 +272,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -289,7 +288,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -306,7 +305,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -323,7 +322,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -336,7 +335,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -348,7 +347,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -419,15 +418,15 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): assert nwBuild._saveDocument(nwBuild.FMT_NWD) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = path.join(nwTempBuild, "1_LoremIpsum.nwd") - refFile = path.join(nwRef, "build", "1_LoremIpsum.nwd") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") + testFile = os.path.join(nwTempBuild, "1_LoremIpsum.nwd") + refFile = os.path.join(nwRef, "build", "1_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = path.join(nwTempBuild, "1_LoremIpsum.htm") - refFile = path.join(nwRef, "build", "1_LoremIpsum.htm") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") + testFile = os.path.join(nwTempBuild, "1_LoremIpsum.htm") + refFile = os.path.join(nwRef, "build", "1_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -458,15 +457,15 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): assert nwBuild._saveDocument(nwBuild.FMT_NWD) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = path.join(nwTempBuild, "2_LoremIpsum.nwd") - refFile = path.join(nwRef, "build", "2_LoremIpsum.nwd") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") + testFile = os.path.join(nwTempBuild, "2_LoremIpsum.nwd") + refFile = os.path.join(nwRef, "build", "2_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = path.join(nwTempBuild, "2_LoremIpsum.htm") - refFile = path.join(nwRef, "build", "2_LoremIpsum.htm") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") + testFile = os.path.join(nwTempBuild, "2_LoremIpsum.htm") + refFile = os.path.join(nwRef, "build", "2_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -491,31 +490,31 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): # Save files that can be compared assert nwBuild._saveDocument(nwBuild.FMT_NWD) - projFile = path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = path.join(nwTempBuild, "3_LoremIpsum.nwd") - refFile = path.join(nwRef, "build", "3_LoremIpsum.nwd") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") + testFile = os.path.join(nwTempBuild, "3_LoremIpsum.nwd") + refFile = os.path.join(nwRef, "build", "3_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = path.join(nwTempBuild, "3_LoremIpsum.htm") - refFile = path.join(nwRef, "build", "3_LoremIpsum.htm") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") + testFile = os.path.join(nwTempBuild, "3_LoremIpsum.htm") + refFile = os.path.join(nwRef, "build", "3_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) # Check the JSON files too at this stage assert nwBuild._saveDocument(nwBuild.FMT_JSON_H) - projFile = path.join(nwLipsum, "Lorem Ipsum.json") - testFile = path.join(nwTempBuild, "3H_LoremIpsum.json") - refFile = path.join(nwRef, "build", "3H_LoremIpsum.json") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") + testFile = os.path.join(nwTempBuild, "3H_LoremIpsum.json") + refFile = os.path.join(nwRef, "build", "3H_LoremIpsum.json") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [8]) assert nwBuild._saveDocument(nwBuild.FMT_JSON_M) - projFile = path.join(nwLipsum, "Lorem Ipsum.json") - testFile = path.join(nwTempBuild, "3M_LoremIpsum.json") - refFile = path.join(nwRef, "build", "3M_LoremIpsum.json") + projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") + testFile = os.path.join(nwTempBuild, "3M_LoremIpsum.json") + refFile = os.path.join(nwRef, "build", "3M_LoremIpsum.json") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [8]) @@ -526,10 +525,10 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): assert nwBuild._saveDocument(nwBuild.FMT_PDF) assert nwBuild._saveDocument(nwBuild.FMT_MD) assert nwBuild._saveDocument(nwBuild.FMT_TXT) - assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.odt")) - assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.pdf")) - assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.md")) - assert path.isfile(path.join(nwLipsum, "Lorem Ipsum.txt")) + assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.odt")) + assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf")) + assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.md")) + assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.txt")) # Close the build tool htmlText = nwBuild.htmlText @@ -581,9 +580,9 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef assert nwGUI.theProject.projTree["73475cb40a568"] is not None - projFile = path.join(nwLipsum, "content", "73475cb40a568.nwd") - testFile = path.join(nwTempGUI, "4_73475cb40a568.nwd") - refFile = path.join(nwRef, "gui", "4_73475cb40a568.nwd") + projFile = os.path.join(nwLipsum, "content", "73475cb40a568.nwd") + testFile = os.path.join(nwTempGUI, "4_73475cb40a568.nwd") + refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -607,9 +606,9 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef assert nwGUI.theProject.projTree["71ee45a3c0db9"] is not None # This should give us back the file as it was before - projFile = path.join(nwLipsum, "content", "71ee45a3c0db9.nwd") - testFile = path.join(nwTempGUI, "4_71ee45a3c0db9.nwd") - refFile = path.join(nwRef, "gui", "4_73475cb40a568.nwd") + projFile = os.path.join(nwLipsum, "content", "71ee45a3c0db9.nwd") + testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd") + refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [1]) @@ -627,21 +626,21 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef assert nwGUI.theProject.projTree["31489056e0916"] is not None assert nwGUI.theProject.projTree["98010bd9270f9"] is not None - projFile = path.join(nwLipsum, "content", "25fc0e7096fc6.nwd") - testFile = path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") - refFile = path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") + projFile = os.path.join(nwLipsum, "content", "25fc0e7096fc6.nwd") + testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") + refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "content", "31489056e0916.nwd") - testFile = path.join(nwTempGUI, "5_31489056e0916.nwd") - refFile = path.join(nwRef, "gui", "5_31489056e0916.nwd") + projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd") + testFile = os.path.join(nwTempGUI, "5_31489056e0916.nwd") + refFile = os.path.join(nwRef, "gui", "5_31489056e0916.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "content", "98010bd9270f9.nwd") - testFile = path.join(nwTempGUI, "5_98010bd9270f9.nwd") - refFile = path.join(nwRef, "gui", "5_98010bd9270f9.nwd") + projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd") + testFile = os.path.join(nwTempGUI, "5_98010bd9270f9.nwd") + refFile = os.path.join(nwRef, "gui", "5_98010bd9270f9.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -661,33 +660,33 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef assert nwGUI.theProject.projTree["2858dcd1057d3"] is not None assert nwGUI.theProject.projTree["2fca346db6561"] is not None - projFile = path.join(nwLipsum, "content", "1a6562590ef19.nwd") - testFile = path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") - refFile = path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") + projFile = os.path.join(nwLipsum, "content", "1a6562590ef19.nwd") + testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") + refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [1]) - projFile = path.join(nwLipsum, "content", "031b4af5197ec.nwd") - testFile = path.join(nwTempGUI, "5_031b4af5197ec.nwd") - refFile = path.join(nwRef, "gui", "5_031b4af5197ec.nwd") + projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd") + testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd") + refFile = os.path.join(nwRef, "gui", "5_031b4af5197ec.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd") - testFile = path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd") - refFile = path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd") + projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd") + testFile = os.path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd") + refFile = os.path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "content", "2858dcd1057d3.nwd") - testFile = path.join(nwTempGUI, "5_2858dcd1057d3.nwd") - refFile = path.join(nwRef, "gui", "5_2858dcd1057d3.nwd") + projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd") + testFile = os.path.join(nwTempGUI, "5_2858dcd1057d3.nwd") + refFile = os.path.join(nwRef, "gui", "5_2858dcd1057d3.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwLipsum, "content", "2fca346db6561.nwd") - testFile = path.join(nwTempGUI, "5_2fca346db6561.nwd") - refFile = path.join(nwRef, "gui", "5_2fca346db6561.nwd") + projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd") + testFile = os.path.join(nwTempGUI, "5_2fca346db6561.nwd") + refFile = os.path.join(nwRef, "gui", "5_2fca346db6561.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -795,7 +794,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): qtbot.wait(stepDelay) qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) - projPath = path.join(nwMinimal, "Test Minimal") + projPath = os.path.join(nwMinimal, "Test Minimal") assert storagePage.projPath.text() == projPath # Setting projPath should activate the button @@ -939,7 +938,7 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwLoad._keyPressDelete() assert nwLoad.listBox.topLevelItemCount() == recentCount - 1 - getFile = path.join(nwMinimal, "nwProject.nwx") + getFile = os.path.join(nwMinimal, "nwProject.nwx") monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwargs: (getFile, None)) qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton) assert nwLoad.openPath == nwMinimal @@ -1109,9 +1108,9 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC # qtbot.stopForInteraction() nwGUI.closeMain() - refConf = path.join(nwRef, "novelwriter_prefs.conf") - projConf = path.join(nwGUI.mainConf.confPath, "novelwriter.conf") - testConf = path.join(nwTemp, "novelwriter_prefs.conf") + refConf = os.path.join(nwRef, "novelwriter_prefs.conf") + projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf") + testConf = os.path.join(nwTemp, "novelwriter_prefs.conf") copyfile(projConf, testConf) ignoreLines = [ 2, # Timestamp diff --git a/tests/test_gui.py b/tests/test_gui.py index c3d02811..0cd9dd87 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -5,11 +5,11 @@ import nw import pytest import logging +import os from shutil import copyfile from nwtools import cmpFiles -from os import path from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon from PyQt5.QtWidgets import ( @@ -57,19 +57,19 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): nwGUI.close() # Log file - logFile = path.join(nwTemp, "logFile.log") - bakFile = path.join(nwTemp, "logFile.log.bak") + logFile = os.path.join(nwTemp, "logFile.log") + bakFile = os.path.join(nwTemp, "logFile.log.bak") nwGUI = nw.main( ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) - assert path.isfile(logFile) + assert os.path.isfile(logFile) nwGUI = nw.main( ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) - assert path.isfile(bakFile) - assert path.isfile(logFile) + assert os.path.isfile(bakFile) + assert os.path.isfile(logFile) nwGUI.closeMain() nwGUI.close() @@ -116,9 +116,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert not nwGUI.theProject.spellCheck # Check the files - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempGUI, "0_nwProject.nwx") - refFile = path.join(nwRef, "gui", "0_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempGUI, "0_nwProject.nwx") + refFile = os.path.join(nwRef, "gui", "0_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) qtbot.wait(stepDelay) @@ -135,7 +135,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert len(nwGUI.theProject.projTree._treeRoots) == 4 assert nwGUI.theProject.projTree.trashRoot() is None assert nwGUI.theProject.projPath == nwFuncTemp - assert nwGUI.theProject.projMeta == path.join(nwFuncTemp, "meta") + assert nwGUI.theProject.projMeta == os.path.join(nwFuncTemp, "meta") assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "New Project" assert nwGUI.theProject.bookTitle == "" @@ -358,33 +358,33 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert nwGUI.saveProject() # Check the files - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempGUI, "1_nwProject.nwx") - refFile = path.join(nwRef, "gui", "1_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempGUI, "1_nwProject.nwx") + refFile = os.path.join(nwRef, "gui", "1_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - projFile = path.join(nwFuncTemp, "content", "031b4af5197ec.nwd") - testFile = path.join(nwTempGUI, "1_031b4af5197ec.nwd") - refFile = path.join(nwRef, "gui", "1_031b4af5197ec.nwd") + projFile = os.path.join(nwFuncTemp, "content", "031b4af5197ec.nwd") + testFile = os.path.join(nwTempGUI, "1_031b4af5197ec.nwd") + refFile = os.path.join(nwRef, "gui", "1_031b4af5197ec.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwFuncTemp, "content", "1a6562590ef19.nwd") - testFile = path.join(nwTempGUI, "1_1a6562590ef19.nwd") - refFile = path.join(nwRef, "gui", "1_1a6562590ef19.nwd") + projFile = os.path.join(nwFuncTemp, "content", "1a6562590ef19.nwd") + testFile = os.path.join(nwTempGUI, "1_1a6562590ef19.nwd") + refFile = os.path.join(nwRef, "gui", "1_1a6562590ef19.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") - testFile = path.join(nwTempGUI, "1_0e17daca5f3e1.nwd") - refFile = path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd") + projFile = os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") + testFile = os.path.join(nwTempGUI, "1_0e17daca5f3e1.nwd") + refFile = os.path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = path.join(nwFuncTemp, "content", "41cfc0d1f2d12.nwd") - testFile = path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd") - refFile = path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd") + projFile = os.path.join(nwFuncTemp, "content", "41cfc0d1f2d12.nwd") + testFile = os.path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd") + refFile = os.path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -625,7 +625,7 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp): nwGUI.openDocument("73475cb40a568") nwGUI.docEditor.setText("# Hello World\n") nwGUI.saveDocument() - assert path.isfile(path.join(nwMinimal, "content", "73475cb40a568.nwd")) + assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) # Delete the items we added earlier nwTree.clearSelection() @@ -640,17 +640,17 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp): assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder # The file is in trash, empty it - assert path.isfile(path.join(nwMinimal, "content", "73475cb40a568.nwd")) + assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) assert nwTree.emptyTrash() assert not nwTree.emptyTrash() # Already empty - assert not path.isfile(path.join(nwMinimal, "content", "73475cb40a568.nwd")) + assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder # Close the project nwGUI.closeProject() # Add an orphaned file - orphFile = path.join(nwMinimal, "content", "1234567890abc.nwd") + orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd") with open(orphFile, mode="w+", encoding="utf8") as outFile: outFile.write("# Hello World\n") @@ -1102,7 +1102,7 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): assert not nwGUI.importDocument() # Then a valid path, but bot a file that exists - theFile = path.join(nwTemp, "import.txt") + theFile = os.path.join(nwTemp, "import.txt") monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: [theFile]) assert not nwGUI.importDocument() @@ -1145,7 +1145,7 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): assert len(theBits) == 3 assert theBits[0] == "File details for the currently open file" assert theBits[1] == "Handle: 0e17daca5f3e1" - assert theBits[2] == "Location: %s" % path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") + assert theBits[2] == "Location: %s" % os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") # qtbot.stopForInteraction() nwGUI.closeMain() diff --git a/tests/test_project.py b/tests/test_project.py index fc980d96..14edf1fe 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -3,7 +3,8 @@ """ import pytest -from os import path, mkdir, listdir +import os + from shutil import copyfile from zipfile import ZipFile @@ -18,9 +19,9 @@ from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): """Test that a basic project can be created, and opened and saved. """ - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempProj, "1_nwProject.nwx") - refFile = path.join(nwRef, "proj", "1_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempProj, "1_nwProject.nwx") + refFile = os.path.join(nwRef, "proj", "1_nwProject.nwx") theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) @@ -64,9 +65,9 @@ def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy): """Check that new root folders can be added to the project. """ - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempProj, "2_nwProject.nwx") - refFile = path.join(nwRef, "proj", "2_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempProj, "2_nwProject.nwx") + refFile = os.path.join(nwRef, "proj", "2_nwProject.nwx") theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) @@ -98,9 +99,9 @@ def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy): def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy): """Check that new files can be added to the project. """ - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempProj, "3_nwProject.nwx") - refFile = path.join(nwRef, "proj", "3_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempProj, "3_nwProject.nwx") + refFile = os.path.join(nwRef, "proj", "3_nwProject.nwx") theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) @@ -126,9 +127,9 @@ def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy): """Create a new project from a project wizard dictionary. Custom type with chapters and scenes. """ - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempProj, "4_nwProject.nwx") - refFile = path.join(nwRef, "proj", "4_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempProj, "4_nwProject.nwx") + refFile = os.path.join(nwRef, "proj", "4_nwProject.nwx") projData = { "projName": "Test Custom", @@ -165,9 +166,9 @@ def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy): """Create a new project from a project wizard dictionary. Custom type without chapters, but with scenes. """ - projFile = path.join(nwFuncTemp, "nwProject.nwx") - testFile = path.join(nwTempProj, "5_nwProject.nwx") - refFile = path.join(nwRef, "proj", "5_nwProject.nwx") + projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + testFile = os.path.join(nwTempProj, "5_nwProject.nwx") + refFile = os.path.join(nwRef, "proj", "5_nwProject.nwx") projData = { "projName": "Test Custom", @@ -200,9 +201,9 @@ def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy): assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) @pytest.mark.project -def testProjectNewSample(nwFuncTemp, nwRef, nwConf, nwDummy): +def testProjectNewSampleA(nwFuncTemp, nwConf, nwDummy, nwTemp): """Check that we can create a new project can be created from the - provided sample project. + provided sample project via a zip file. """ projData = { "projName": "Test Sample", @@ -217,11 +218,99 @@ def testProjectNewSample(nwFuncTemp, nwRef, nwConf, nwDummy): theProject.projTree.setSeed(42) theProject.mainConf = nwConf + # Sample set, but no path + assert not theProject.newProject({"popSample": True}) + + # Force the lookup path for assets to our temp folder + srcSample = os.path.abspath(os.path.join(nwConf.appRoot, "sample")) + dstSample = os.path.join(nwTemp, "sample.zip") + nwConf.assetPath = nwTemp + + # Create and open a defective zip file + with open(dstSample, mode="w+") as outFile: + outFile.write("foo") + + assert not theProject.newProject(projData) + os.unlink(dstSample) + + # Create a real zip file, and unpack it + with ZipFile(dstSample, "w") as zipObj: + zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") + for docFile in os.listdir(os.path.join(srcSample, "content")): + srcDoc = os.path.join(srcSample, "content", docFile) + zipObj.write(srcDoc, "content/"+docFile) + assert theProject.newProject(projData) assert theProject.openProject(nwFuncTemp) assert theProject.projName == "Sample Project" assert theProject.saveProject() assert theProject.closeProject() + os.unlink(dstSample) + +@pytest.mark.project +def testProjectNewSampleB(monkeypatch, nwFuncTemp, nwConf, nwDummy, nwTemp): + """Check that we can create a new project can be created from the + provided sample project folder. + """ + projData = { + "projName": "Test Sample", + "projTitle": "Test Novel", + "projAuthors": "Jane Doe\nJohn Doh\n", + "projPath": nwFuncTemp, + "popSample": True, + "popMinimal": False, + "popCustom": False, + } + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + theProject.mainConf = nwConf + + # Make sure we do not pick up the nw/assets/sample.zip file + nwConf.assetPath = nwTemp + + # Set a fake project file name + monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx") + assert not theProject.newProject(projData) + + monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx") + assert theProject.newProject(projData) + assert theProject.openProject(nwFuncTemp) + assert theProject.projName == "Sample Project" + assert theProject.saveProject() + assert theProject.closeProject() + + # Misdirect the appRoot path so neither is possible + nwConf.appRoot = nwTemp + assert not theProject.newProject(projData) + +@pytest.mark.project +def testProjectMethods(monkeypatch, nwMinimal, nwDummy): + """Test other project class methods and functions. + """ + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwMinimal) + assert theProject.projPath == nwMinimal + + # Setting project path + assert theProject.setProjectPath(None) + assert theProject.projPath is None + assert theProject.setProjectPath("") + assert theProject.projPath is None + assert theProject.setProjectPath("~") + assert theProject.projPath == os.path.expanduser("~") + + # Create a new folder and populate it + projPath = os.path.join(nwMinimal, "dummy1") + assert theProject.setProjectPath(projPath, newProject=True) + + # Make the os.mkdir fail + def altMkdir(*args): + raise Exception("Oops!") + + monkeypatch.setattr("os.mkdir", altMkdir) + projPath = os.path.join(nwMinimal, "dummy2") + assert not theProject.setProjectPath(projPath, newProject=True) @pytest.mark.project def testDocMeta(nwDummy, nwLipsum): @@ -252,7 +341,7 @@ def testDocMeta(nwDummy, nwLipsum): @pytest.mark.project def testSpellEnchant(nwTemp, nwConf): - wList = path.join(nwTemp, "wordlist.txt") + wList = os.path.join(nwTemp, "wordlist.txt") with open(wList, mode="w") as wFile: wFile.write("a_word\nb_word\nc_word\n") @@ -277,7 +366,7 @@ def testSpellEnchant(nwTemp, nwConf): @pytest.mark.project def testSpellSimple(nwTemp, nwConf): - wList = path.join(nwTemp, "wordlist.txt") + wList = os.path.join(nwTemp, "wordlist.txt") with open(wList, mode="w") as wFile: wFile.write("a_word\nb_word\nc_word\n") @@ -320,7 +409,7 @@ def testProjectOptions(nwDummy, nwLipsum): assert str(theOpts.theState) == r"{}" # Read Invalid Settings and Filter - stateFile = path.join(theProject.projMeta, nwFiles.OPTS_FILE) + stateFile = os.path.join(theProject.projMeta, nwFiles.OPTS_FILE) with open(stateFile, mode="w", encoding="utf8") as outFile: outFile.write( r'{"GuiProjectSettings": {"winWidth": 100, "winHeight": 50}, "NoGroup": {"NoName": 0}}' @@ -376,28 +465,28 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum): assert theProject.closeProject() # First Item with Meta Data - orphPath = path.join(nwLipsum, "content", "636b6aa9b697b.nwd") + orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") with open(orphPath, mode="w", encoding="utf8") as outFile: outFile.write(r"%%~ 5eaea4e8cdee8:15c4492bd5107:WORLD:NOTE:Mars") outFile.write("\n") # Second Item without Meta Data - orphPath = path.join(nwLipsum, "content", "736b6aa9b697b.nwd") + orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd") with open(orphPath, mode="w", encoding="utf8") as outFile: outFile.write("\n") # Invalid File Name - dummyPath = path.join(nwLipsum, "content", "636b6aa9b697b.txt") + dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt") with open(dummyPath, mode="w", encoding="utf8") as outFile: outFile.write("\n") # Invalid File Name - dummyPath = path.join(nwLipsum, "content", "636b6aa9b697bb.nwd") + dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd") with open(dummyPath, mode="w", encoding="utf8") as outFile: outFile.write("\n") # Invalid File Name - dummyPath = path.join(nwLipsum, "content", "abcdefghijklm.nwd") + dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd") with open(dummyPath, mode="w", encoding="utf8") as outFile: outFile.write("\n") @@ -441,84 +530,84 @@ def testProjectOldFormat(nwDummy, nwOldProj): # Create dummy files for known legacy files deleteFiles = [ - path.join(nwOldProj, "cache", "nwProject.nwx.0"), - path.join(nwOldProj, "cache", "nwProject.nwx.1"), - path.join(nwOldProj, "cache", "nwProject.nwx.2"), - path.join(nwOldProj, "cache", "nwProject.nwx.3"), - path.join(nwOldProj, "cache", "nwProject.nwx.4"), - path.join(nwOldProj, "cache", "nwProject.nwx.5"), - path.join(nwOldProj, "cache", "nwProject.nwx.6"), - path.join(nwOldProj, "cache", "nwProject.nwx.7"), - path.join(nwOldProj, "cache", "nwProject.nwx.8"), - path.join(nwOldProj, "cache", "nwProject.nwx.9"), - path.join(nwOldProj, "meta", "mainOptions.json"), - path.join(nwOldProj, "meta", "exportOptions.json"), - path.join(nwOldProj, "meta", "outlineOptions.json"), - path.join(nwOldProj, "meta", "timelineOptions.json"), - path.join(nwOldProj, "meta", "docMergeOptions.json"), - path.join(nwOldProj, "meta", "sessionLogOptions.json"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.0"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.1"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.2"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.3"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.4"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.5"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.6"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.7"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.8"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.9"), + os.path.join(nwOldProj, "meta", "mainOptions.json"), + os.path.join(nwOldProj, "meta", "exportOptions.json"), + os.path.join(nwOldProj, "meta", "outlineOptions.json"), + os.path.join(nwOldProj, "meta", "timelineOptions.json"), + os.path.join(nwOldProj, "meta", "docMergeOptions.json"), + os.path.join(nwOldProj, "meta", "sessionLogOptions.json"), ] # Add some files that shouldn't be there - deleteFiles.append(path.join(nwOldProj, "data_f", "whatnow.nwd")) - deleteFiles.append(path.join(nwOldProj, "data_f", "whatnow.txt")) + deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd")) + deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt")) # Add some folders that shouldn't be there - mkdir(path.join(nwOldProj, "stuff")) - mkdir(path.join(nwOldProj, "data_1", "stuff")) + os.mkdir(os.path.join(nwOldProj, "stuff")) + os.mkdir(os.path.join(nwOldProj, "data_1", "stuff")) # Create dummy files - mkdir(path.join(nwOldProj, "cache")) + os.mkdir(os.path.join(nwOldProj, "cache")) for aFile in deleteFiles: with open(aFile, mode="w+", encoding="utf8") as outFile: outFile.write("Hi") for aFile in deleteFiles: - assert path.isfile(aFile) + assert os.path.isfile(aFile) # Open project and check that files that are not supposed to be # there have been removed assert theProject.openProject(nwOldProj) for aFile in deleteFiles: - assert not path.isfile(aFile) + assert not os.path.isfile(aFile) - assert not path.isdir(path.join(nwOldProj, "data_1", "stuff")) - assert not path.isdir(path.join(nwOldProj, "data_1")) - assert not path.isdir(path.join(nwOldProj, "data_7")) - assert not path.isdir(path.join(nwOldProj, "data_8")) - assert not path.isdir(path.join(nwOldProj, "data_9")) - assert not path.isdir(path.join(nwOldProj, "data_a")) - assert not path.isdir(path.join(nwOldProj, "data_f")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_1")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_7")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_8")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_9")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_a")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_f")) # Check stuff that has been moved - assert path.isdir(path.join(nwOldProj, "junk")) - assert path.isdir(path.join(nwOldProj, "junk", "stuff")) - assert path.isfile(path.join(nwOldProj, "junk", "whatnow.nwd")) - assert path.isfile(path.join(nwOldProj, "junk", "whatnow.txt")) + assert os.path.isdir(os.path.join(nwOldProj, "junk")) + assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff")) + assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt")) # Check that files we want to keep are in the right place - assert path.isdir(path.join(nwOldProj, "cache")) - assert path.isdir(path.join(nwOldProj, "content")) - assert path.isdir(path.join(nwOldProj, "meta")) + assert os.path.isdir(os.path.join(nwOldProj, "cache")) + assert os.path.isdir(os.path.join(nwOldProj, "content")) + assert os.path.isdir(os.path.join(nwOldProj, "meta")) - assert path.isfile(path.join(nwOldProj, "content", "f528d831f5b24.nwd")) - assert path.isfile(path.join(nwOldProj, "content", "88124a4292d8b.nwd")) - assert path.isfile(path.join(nwOldProj, "content", "91239bf2f8b69.nwd")) - assert path.isfile(path.join(nwOldProj, "content", "19752e7f9d8af.nwd")) - assert path.isfile(path.join(nwOldProj, "content", "a764d5acf5a21.nwd")) - assert path.isfile(path.join(nwOldProj, "content", "9058ae29f0dfd.nwd")) - assert path.isfile(path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd")) - assert path.isfile(path.join(nwOldProj, "meta", "tagsIndex.json")) - assert path.isfile(path.join(nwOldProj, "meta", "sessionInfo.log")) + assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json")) + assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log")) # Close the project theProject.closeProject() # Check that new files have been created - assert path.isfile(path.join(nwOldProj, "meta", "guiOptions.json")) - assert path.isfile(path.join(nwOldProj, "meta", "sessionStats.log")) - assert path.isfile(path.join(nwOldProj, "ToC.json")) - assert path.isfile(path.join(nwOldProj, "ToC.txt")) + assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json")) + assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log")) + assert os.path.isfile(os.path.join(nwOldProj, "ToC.json")) + assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt")) @pytest.mark.project def testProjectBackup(nwDummy, nwMinimal, nwTemp): @@ -541,7 +630,7 @@ def testProjectBackup(nwDummy, nwMinimal, nwTemp): assert not theProject.zipIt(doNotify=False) # Non-existent folder - theProject.mainConf.backupPath = path.join(nwTemp, "nonexistent") + theProject.mainConf.backupPath = os.path.join(nwTemp, "nonexistent") theProject.projName = "Test Minimal" assert not theProject.zipIt(doNotify=False) @@ -553,7 +642,7 @@ def testProjectBackup(nwDummy, nwMinimal, nwTemp): theProject.mainConf.backupPath = nwTemp assert theProject.zipIt(doNotify=False) - theFiles = listdir(path.join(nwTemp, "Test Minimal")) + theFiles = os.listdir(os.path.join(nwTemp, "Test Minimal")) assert len(theFiles) == 1 theZip = theFiles[0] @@ -561,10 +650,10 @@ def testProjectBackup(nwDummy, nwMinimal, nwTemp): assert theZip[-4:] == ".zip" # Extract the archive - with ZipFile(path.join(nwTemp, "Test Minimal", theZip), "r") as inZip: - inZip.extractall(path.join(nwTemp, "extract")) + with ZipFile(os.path.join(nwTemp, "Test Minimal", theZip), "r") as inZip: + inZip.extractall(os.path.join(nwTemp, "extract")) # Check that the main project file was restored assert cmpFiles( - path.join(nwMinimal, "nwProject.nwx"), path.join(nwTemp, "extract", "nwProject.nwx") + os.path.join(nwMinimal, "nwProject.nwx"), os.path.join(nwTemp, "extract", "nwProject.nwx") ) From e721fbbdb7d6d36402b08c772008b3832ae2e19b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Sep 2020 23:47:06 +0200 Subject: [PATCH 34/51] Add coverage of some partially covered functions --- nw/core/project.py | 2 +- nw/core/tools.py | 1 - tests/test_project.py | 15 ++++++++++++++- tests/test_tools.py | 11 +++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index cf8dd32f..953d7879 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -981,7 +981,7 @@ class NWProject(): def setProjBackup(self, doBackup): """Set whether projects should be backed up or not. The user - will notified in case dependant settings are missing. + will be notified in case required settings are missing. """ self.doBackup = doBackup if doBackup: diff --git a/nw/core/tools.py b/nw/core/tools.py index c5ef9eed..ce30de96 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -121,7 +121,6 @@ def numberToWord(numVal, theLanguage): numWord = _numberToWordEN(numVal) else: numWord = _numberToWordEN(numVal) - # print("%4d : %s" % (numVal, numWord)) return numWord def _numberToWordEN(numVal): diff --git a/tests/test_project.py b/tests/test_project.py index 14edf1fe..21423ffe 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -304,7 +304,7 @@ def testProjectMethods(monkeypatch, nwMinimal, nwDummy): projPath = os.path.join(nwMinimal, "dummy1") assert theProject.setProjectPath(projPath, newProject=True) - # Make the os.mkdir fail + # Make os.mkdir fail def altMkdir(*args): raise Exception("Oops!") @@ -312,6 +312,19 @@ def testProjectMethods(monkeypatch, nwMinimal, nwDummy): projPath = os.path.join(nwMinimal, "dummy2") assert not theProject.setProjectPath(projPath, newProject=True) + # Project Name + assert theProject.setProjectName(" A Name ") + assert theProject.projName == "A Name" + + # Project Title + assert theProject.setBookTitle(" A Title ") + assert theProject.bookTitle == "A Title" + + # Project Authors + assert not theProject.setBookAuthors([]) + assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ") + assert theProject.bookAuthors == ["Jane Doe", "John Doh"] + @pytest.mark.project def testDocMeta(nwDummy, nwLipsum): """Check that the document meta data string is parsed correctly. diff --git a/tests/test_tools.py b/tests/test_tools.py index bd79fb69..bfff8f3f 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -8,6 +8,8 @@ from nw.core.tools import countWords, numberToRoman, numberToWord @pytest.mark.core def testCountWords(): + """Test the word counter and the exclusion filers. + """ testText = ( "# Heading One\n" "## Heading Two\n" @@ -33,6 +35,8 @@ def testCountWords(): @pytest.mark.core def testNumberWords(): + """Test the conversion of integer to English words. + """ assert numberToWord(0, "en") == "Zero" assert numberToWord(1, "en") == "One" assert numberToWord(2, "en") == "Two" @@ -60,8 +64,15 @@ def testNumberWords(): assert numberToWord(142, "en") == "One Hundred Forty-Two" assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine" + # Check a few with a nonsense language setting + assert numberToWord(1, "foo") == "One" + assert numberToWord(2, "foo") == "Two" + assert numberToWord(3, "foo") == "Three" + @pytest.mark.core def testRomanNumbers(): + """Test conversion of integers to Roman numbers. + """ assert numberToRoman(None, False) == "NAN" assert numberToRoman(0, False) == "OOR" assert numberToRoman(1, False) == "I" From 1dc2a4958137ff90cf5268c20816c4df1e1888d6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 29 Sep 2020 23:51:13 +0200 Subject: [PATCH 35/51] Fixed code style error (too long line) --- nw/gui/theme.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 3a511907..0af7df5e 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -376,7 +376,9 @@ class GuiTheme: confParser = configparser.ConfigParser() for themeDir in os.listdir(os.path.join(self.mainConf.themeRoot, self.guiPath)): - themeConf = os.path.join(self.mainConf.themeRoot, self.guiPath, themeDir, self.confName) + themeConf = os.path.join( + self.mainConf.themeRoot, self.guiPath, themeDir, self.confName + ) logger.verbose("Checking theme config for '%s'" % themeDir) try: with open(themeConf, mode="r", encoding="utf8") as inFile: From d382e70d72f2213bcf296614dc429ba819da59e3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Sep 2020 19:53:00 +0200 Subject: [PATCH 36/51] Fix a potential issue in the test suite --- tests/nwtools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/nwtools.py b/tests/nwtools.py index 0b8896a3..e35d9e08 100644 --- a/tests/nwtools.py +++ b/tests/nwtools.py @@ -6,9 +6,12 @@ from itertools import chain from PyQt5.QtWidgets import qApp -def cmpFiles(fileOne, fileTwo, ignoreLines=[]): +def cmpFiles(fileOne, fileTwo, ignoreLines=None): """Compare two files, but optionally ignore lines given by a list. """ + if ignoreLines is None: + ignoreLines = [] + try: foOne = open(fileOne, mode="r", encoding="utf8") except Exception as e: From 6fc5dad938c20aacc5ad74e4f6fbdc94d8d3a565 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Sep 2020 21:50:59 +0200 Subject: [PATCH 37/51] Fixed some potential issues with getting the trash folder handle, and made some minor improvements to index class methods --- nw/core/index.py | 44 ++++++++++++++++++-------------------------- nw/core/tree.py | 7 +++++++ nw/gui/projtree.py | 4 ++-- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 06cddf60..8925b07e 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -279,8 +279,8 @@ class NWIndex(): if theItem.itemLayout == nwItemLayout.NO_LAYOUT: logger.error("Not indexing no-layout item %s" % tHandle) return False - if theRoot is None: - logger.error("Not indexing homeless item %s" % tHandle) + if theItem.parHandle is None: + logger.error("Not indexing orphaned item %s" % tHandle) return False # Run word counter for the whole text @@ -288,7 +288,7 @@ class NWIndex(): self.textCounts[tHandle] = [cC, wC, pC] # If the file is archived or trashed, we don't index the file itself - if theItem.parHandle == self.theProject.projTree.trashRoot(): + if self.theProject.projTree.isTrashRoot(theItem.parHandle): logger.error("Not indexing trash item %s" % tHandle) return False if theRoot.itemClass == nwItemClass.ARCHIVE: @@ -459,16 +459,12 @@ class NWIndex(): """Validate and save the information about a reference to a tag in another file. """ - isValid, theBits, thePos = self.scanThis(aLine) + isValid, theBits, _ = self.scanThis(aLine) if not isValid or len(theBits) == 0: return False sTitle = "T%06d" % nTitle - if sTitle not in self.refIndex[tHandle]: - logger.error("Cannot save tags to file %s, no title %s" % (tHandle, sTitle)) - return False - - if theBits[0] != nwKeyWords.TAG_KEY: + if sTitle in self.refIndex[tHandle] and theBits[0] != nwKeyWords.TAG_KEY: for aVal in theBits[1:]: self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal]) @@ -574,15 +570,14 @@ class NWIndex(): """ theStructure = [] for tItem in self.theProject.projTree: - if tItem is None: - continue - if not tItem.isExported and skipExcluded: - continue - tHandle = tItem.itemHandle - if tHandle not in self.novelIndex: - continue - for sTitle in sorted(self.novelIndex[tHandle].keys()): - theStructure.append("%s:%s" % (tHandle, sTitle)) + if tItem is not None: + if not tItem.isExported and skipExcluded: + continue + tHandle = tItem.itemHandle + if tHandle not in self.novelIndex: + continue + for sTitle in sorted(self.novelIndex[tHandle].keys()): + theStructure.append("%s:%s" % (tHandle, sTitle)) return theStructure @@ -624,14 +619,11 @@ class NWIndex(): if tHandle not in self.refIndex: return theRefs - try: - for refTitle in self.refIndex[tHandle]: - for nLine, tKey, tTag in self.refIndex[tHandle][refTitle]["tags"]: - if sTitle is None or sTitle == refTitle: - theRefs[tKey].append(tTag) - except Exception as e: - logger.error("Failed to generate reference list") - logger.error(str(e)) + for refTitle in self.refIndex[tHandle]: + theTags = self.refIndex[tHandle][refTitle].get("tags", None) + for aTag in theTags: + if len(aTag) == 3 and (sTitle is None or sTitle == refTitle): + theRefs[aTag[1]].append(aTag[2]) return theRefs diff --git a/nw/core/tree.py b/nw/core/tree.py index 9389d2db..c51ae16c 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -213,6 +213,13 @@ class NWTree(): return self._trashRoot return None + def isTrashRoot(self, tHandle): + """Check if a handle is the trash folder. + """ + if self._trashRoot is None: + return False + return tHandle == self._trashRoot + def archiveRoot(self): """Returns the handle of the archive folder, or None if there isn't one. diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 00b81dac..65c4bb1f 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -209,7 +209,7 @@ class GuiProjectTree(QTreeWidget): ) return False - if pHandle == self.theProject.projTree.trashRoot(): + if self.theProject.projTree.isTrashRoot(pHandle): self.makeAlert( "Cannot add new files or folders to the %s folder." % ( nwLabels.CLASS_NAME[nwItemClass.TRASH] @@ -411,7 +411,7 @@ class GuiProjectTree(QTreeWidget): return False pHandle = nwItemS.parHandle - if pHandle is not None and pHandle == self.theProject.projTree.trashRoot(): + if self.theProject.projTree.isTrashRoot(pHandle): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False From 9bf50cae34ad7ee8dbde376c09f59f873f2b7e19 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Sep 2020 21:51:16 +0200 Subject: [PATCH 38/51] Improved test coverage of index class --- tests/test_index.py | 261 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 221 insertions(+), 40 deletions(-) diff --git a/tests/test_index.py b/tests/test_index.py index ff3e9260..084a9d39 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -6,11 +6,12 @@ import pytest from nw.core.project import NWProject from nw.core.index import NWIndex -from nw.constants import nwItemClass +from nw.constants import nwItemClass, nwItemLayout @pytest.mark.project def testIndexScanThis(nwMinimal, nwDummy): - + """Test the tag scanner function scanThis. + """ theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) @@ -58,7 +59,8 @@ def testIndexScanThis(nwMinimal, nwDummy): @pytest.mark.project def testIndexCheckThese(nwMinimal, nwDummy): - + """Test the tag checker function checkThese. + """ theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) @@ -92,8 +94,184 @@ def testIndexCheckThese(nwMinimal, nwDummy): assert theProject.closeProject() @pytest.mark.project -def testIndexMeta(nwMinimal, nwDummy): +def testIndexScanText(nwMinimal, nwDummy): + """Check the index data extraction functions. + """ + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwMinimal) + theIndex = NWIndex(theProject, nwDummy) + + # Some items for fail to scan tests + dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c") + xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c") + xItem = theProject.projTree[xHandle] + xItem.setLayout(nwItemLayout.NO_LAYOUT) + + # Check invalid data + assert not theIndex.scanText(None, "Hello World!") + assert not theIndex.scanText(dHandle, "Hello World!") + assert not theIndex.scanText(xHandle, "Hello World!") + + xItem.setLayout(nwItemLayout.SCENE) + xItem.setParent(None) + assert not theIndex.scanText(xHandle, "Hello World!") + + # Create the trash folder + tHandle = theProject.trashFolder() + assert theProject.projTree[tHandle] is not None + xItem.setParent(tHandle) + assert not theIndex.scanText(xHandle, "Hello World!") + + # Create the archive root + aHandle = theProject.newRoot("Outtakes", nwItemClass.ARCHIVE) + assert theProject.projTree[aHandle] is not None + xItem.setParent(aHandle) + assert not theIndex.scanText(xHandle, "Hello World!") + + # Make some usable items + nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") + cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c") + + # Index correct text + assert theIndex.scanText(cHandle, ( + "# Jane Smith\n" + "@tag: Jane\n" + )) + assert theIndex.scanText(nHandle, ( + "# Hello World!\n" + "@pov: Jane\n" + "@char: Jane\n\n" + "% this is a comment\n\n" + "This is a story about Jane Smith.\n\n" + "Well, not really.\n" + )) + assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle + assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" + + # Check that title sections are indexed properly + assert theIndex.scanText(nHandle, ( + "# Title One\n\n" + "% synopsis: Synopsis One.\n\n" + "Paragraph One.\n\n" + "## Title Two\n\n" + "% synopsis: Synopsis Two.\n\n" + "Paragraph Two.\n\n" + "### Title Three\n\n" + "% synopsis: Synopsis Three.\n\n" + "Paragraph Three.\n\n" + "#### Title Four\n\n" + "% synopsis: Synopsis Four.\n\n" + "Paragraph Four.\n\n" + "##### Title Five\n\n" # Not interpreted as a title, the hashes is counted as a word + "Paragraph Five.\n\n" + )) + assert theIndex.refIndex[nHandle].get("T000000", None) is not None # Always there + assert theIndex.refIndex[nHandle].get("T000001", None) is not None # Heading 1 + assert theIndex.refIndex[nHandle].get("T000002", None) is None + assert theIndex.refIndex[nHandle].get("T000003", None) is None + assert theIndex.refIndex[nHandle].get("T000004", None) is None + assert theIndex.refIndex[nHandle].get("T000005", None) is None + assert theIndex.refIndex[nHandle].get("T000006", None) is None + assert theIndex.refIndex[nHandle].get("T000007", None) is not None # Heading 2 + assert theIndex.refIndex[nHandle].get("T000008", None) is None + assert theIndex.refIndex[nHandle].get("T000009", None) is None + assert theIndex.refIndex[nHandle].get("T000010", None) is None + assert theIndex.refIndex[nHandle].get("T000011", None) is None + assert theIndex.refIndex[nHandle].get("T000012", None) is None + assert theIndex.refIndex[nHandle].get("T000013", None) is not None # Heading 3 + assert theIndex.refIndex[nHandle].get("T000014", None) is None + assert theIndex.refIndex[nHandle].get("T000015", None) is None + assert theIndex.refIndex[nHandle].get("T000016", None) is None + assert theIndex.refIndex[nHandle].get("T000017", None) is None + assert theIndex.refIndex[nHandle].get("T000018", None) is None + assert theIndex.refIndex[nHandle].get("T000019", None) is not None # Heading 4 + assert theIndex.refIndex[nHandle].get("T000020", None) is None + assert theIndex.refIndex[nHandle].get("T000021", None) is None + assert theIndex.refIndex[nHandle].get("T000022", None) is None + assert theIndex.refIndex[nHandle].get("T000023", None) is None + assert theIndex.refIndex[nHandle].get("T000024", None) is None + assert theIndex.refIndex[nHandle].get("T000025", None) is None + assert theIndex.refIndex[nHandle].get("T000026", None) is None + + assert theIndex.novelIndex[nHandle]["T000001"]["level"] == "H1" + assert theIndex.novelIndex[nHandle]["T000007"]["level"] == "H2" + assert theIndex.novelIndex[nHandle]["T000013"]["level"] == "H3" + assert theIndex.novelIndex[nHandle]["T000019"]["level"] == "H4" + + assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Title One" + assert theIndex.novelIndex[nHandle]["T000007"]["title"] == "Title Two" + assert theIndex.novelIndex[nHandle]["T000013"]["title"] == "Title Three" + assert theIndex.novelIndex[nHandle]["T000019"]["title"] == "Title Four" + + assert theIndex.novelIndex[nHandle]["T000001"]["layout"] == "SCENE" + assert theIndex.novelIndex[nHandle]["T000007"]["layout"] == "SCENE" + assert theIndex.novelIndex[nHandle]["T000013"]["layout"] == "SCENE" + assert theIndex.novelIndex[nHandle]["T000019"]["layout"] == "SCENE" + + assert theIndex.novelIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One." + assert theIndex.novelIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two." + assert theIndex.novelIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three." + assert theIndex.novelIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four." + + assert theIndex.novelIndex[nHandle]["T000001"]["cCount"] == 23 + assert theIndex.novelIndex[nHandle]["T000007"]["cCount"] == 23 + assert theIndex.novelIndex[nHandle]["T000013"]["cCount"] == 27 + assert theIndex.novelIndex[nHandle]["T000019"]["cCount"] == 56 + + assert theIndex.novelIndex[nHandle]["T000001"]["wCount"] == 4 + assert theIndex.novelIndex[nHandle]["T000007"]["wCount"] == 4 + assert theIndex.novelIndex[nHandle]["T000013"]["wCount"] == 4 + assert theIndex.novelIndex[nHandle]["T000019"]["wCount"] == 9 + + assert theIndex.novelIndex[nHandle]["T000001"]["pCount"] == 1 + assert theIndex.novelIndex[nHandle]["T000007"]["pCount"] == 1 + assert theIndex.novelIndex[nHandle]["T000013"]["pCount"] == 1 + assert theIndex.novelIndex[nHandle]["T000019"]["pCount"] == 3 + + assert theIndex.scanText(cHandle, ( + "# Title One\n\n" + "@tag: One\n\n" + "% synopsis: Synopsis One.\n\n" + "Paragraph One.\n\n" + )) + assert theIndex.refIndex[cHandle].get("T000000", None) is not None + assert theIndex.refIndex[cHandle].get("T000001", None) is not None + assert theIndex.refIndex[cHandle].get("T000002", None) is None + assert theIndex.refIndex[cHandle].get("T000003", None) is None + assert theIndex.refIndex[cHandle].get("T000004", None) is None + assert theIndex.refIndex[cHandle].get("T000005", None) is None + assert theIndex.refIndex[cHandle].get("T000006", None) is None + assert theIndex.refIndex[cHandle].get("T000007", None) is None + + assert theIndex.noteIndex[cHandle]["T000001"]["level"] == "H1" + assert theIndex.noteIndex[cHandle]["T000001"]["title"] == "Title One" + assert theIndex.noteIndex[cHandle]["T000001"]["layout"] == "NOTE" + assert theIndex.noteIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One." + assert theIndex.noteIndex[cHandle]["T000001"]["cCount"] == 23 + assert theIndex.noteIndex[cHandle]["T000001"]["wCount"] == 4 + assert theIndex.noteIndex[cHandle]["T000001"]["pCount"] == 1 + + assert theIndex.scanText(sHandle, ( + "# Title One\n\n" + "@pov: One\n\n" # Valid + "@char: Two\n\n" # Invalid tag + "@:\n\n" # Invalid line + "% synopsis: Synopsis One.\n\n" + "Paragraph One.\n\n" + )) + assert str(theIndex.refIndex[sHandle]["T000001"]["tags"]) == ( + "[[3, '@pov', 'One'], [5, '@char', 'Two']]" + ) + + assert theProject.closeProject() + +@pytest.mark.project +def testIndexExtractData(nwMinimal, nwDummy): + """Check the index data extraction functions. + """ theProject = NWProject(nwDummy) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) @@ -109,16 +287,11 @@ def testIndexMeta(nwMinimal, nwDummy): assert theIndex.scanText(nHandle, ( "# Hello World!\n" "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" + "@char: Jane\n\n" + "% this is a comment\n\n" + "This is a story about Jane Smith.\n\n" "Well, not really.\n" )) - assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle - assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" # The novel structure should contain the pointer to the novel file header assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle @@ -129,6 +302,10 @@ def testIndexMeta(nwMinimal, nwDummy): assert wC == 12 # Words in text and title only assert pC == 2 # Paragraphs in text only + ## + # getReferences + ## + # Look up an ivalid handle theRefs = theIndex.getReferences("Not a handle") assert theRefs["@pov"] == [] @@ -139,30 +316,41 @@ def testIndexMeta(nwMinimal, nwDummy): assert str(theRefs["@pov"]) == "['Jane']" assert str(theRefs["@char"]) == "['Jane']" + ## + # getBackReferenceList + ## + + # None handle should return an empty dict + assert theIndex.getBackReferenceList(None) == {} + # The character file should have a record of the reference from the novel file theRefs = theIndex.getBackReferenceList(cHandle) assert str(theRefs) == "{'%s': 'T000001'}" % nHandle + ## + # getTagSource + ## + + assert theIndex.getTagSource("Jane") == (cHandle, 2, "T000001") + assert theIndex.getTagSource("John") == (None, 0, "T000000") + + ## + # getCounts for whole text and sections + ## + # Get section counts for a novel file assert theIndex.scanText(nHandle, ( "# Hello World!\n" "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - "\n" + "@char: Jane\n\n" + "% this is a comment\n\n" + "This is a story about Jane Smith.\n\n" + "Well, not really.\n\n" "# Hello World!\n" "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" + "@char: Jane\n\n" + "% this is a comment\n\n" + "This is a story about Jane Smith.\n\n" "Well, not really.\n" )) # Whole document @@ -187,22 +375,15 @@ def testIndexMeta(nwMinimal, nwDummy): assert theIndex.scanText(cHandle, ( "# Hello World!\n" "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" - "Well, not really.\n" - "\n" + "@char: Jane\n\n" + "% this is a comment\n\n" + "This is a story about Jane Smith.\n\n" + "Well, not really.\n\n" "# Hello World!\n" "@pov: Jane\n" - "@char: Jane\n" - "\n" - "% this is a comment\n" - "\n" - "This is a story about Jane Smith.\n" - "\n" + "@char: Jane\n\n" + "% this is a comment\n\n" + "This is a story about Jane Smith.\n\n" "Well, not really.\n" )) # Whole document From 86d5a6f6b2041b1181d14ed635e918d3a91a9bd0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Sep 2020 22:57:00 +0200 Subject: [PATCH 39/51] Coverage of index class 100% --- nw/core/index.py | 21 +- tests/reference/proj/1_tagsIndex.json | 560 ++++++++++++++++++++++++++ tests/test_index.py | 132 ++++++ 3 files changed, 702 insertions(+), 11 deletions(-) create mode 100644 tests/reference/proj/1_tagsIndex.json diff --git a/nw/core/index.py b/nw/core/index.py index 8925b07e..13c6c092 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -29,8 +29,7 @@ import nw import logging import json import os - -from time import time +import time from nw.constants import ( nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert @@ -177,7 +176,7 @@ class NWIndex(): if "textCounts" in theData.keys(): self.textCounts = theData["textCounts"] - nowTime = round(time()) + nowTime = round(time.time()) self.timeNovel = nowTime self.timeNote = nowTime self.timeIndex = nowTime @@ -305,7 +304,7 @@ class NWIndex(): self.refIndex[tHandle] = {} self.refIndex[tHandle]["T000000"] = { "tags" : [], - "updated" : round(time()), + "updated" : round(time.time()), } if itemLayout == nwItemLayout.NOTE: self.noteIndex[tHandle] = {} @@ -360,7 +359,7 @@ class NWIndex(): self._indexWordCounts(tHandle, isNovel, lastText, nTitle) # Update timestamps for index changes - nowTime = round(time()) + nowTime = round(time.time()) self.timeIndex = nowTime if isNovel: self.timeNovel = nowTime @@ -395,7 +394,7 @@ class NWIndex(): sTitle = "T%06d" % nLine self.refIndex[tHandle][sTitle] = { "tags" : [], - "updated" : round(time()), + "updated" : round(time.time()), } theData = { "level" : hDepth, @@ -405,7 +404,7 @@ class NWIndex(): "cCount" : 0, "wCount" : 0, "pCount" : 0, - "updated" : round(time()), + "updated" : round(time.time()), } if hText != "": @@ -429,14 +428,14 @@ class NWIndex(): self.novelIndex[tHandle][sTitle]["cCount"] = cC self.novelIndex[tHandle][sTitle]["wCount"] = wC self.novelIndex[tHandle][sTitle]["pCount"] = pC - self.novelIndex[tHandle][sTitle]["updated"] = round(time()) + self.novelIndex[tHandle][sTitle]["updated"] = round(time.time()) else: if tHandle in self.noteIndex: if sTitle in self.noteIndex[tHandle]: self.noteIndex[tHandle][sTitle]["cCount"] = cC self.noteIndex[tHandle][sTitle]["wCount"] = wC self.noteIndex[tHandle][sTitle]["pCount"] = pC - self.noteIndex[tHandle][sTitle]["updated"] = round(time()) + self.noteIndex[tHandle][sTitle]["updated"] = round(time.time()) return def _indexSynopsis(self, tHandle, isNovel, theText, nTitle): @@ -447,12 +446,12 @@ class NWIndex(): if tHandle in self.novelIndex: if sTitle in self.novelIndex[tHandle]: self.novelIndex[tHandle][sTitle]["synopsis"] = theText - self.novelIndex[tHandle][sTitle]["updated"] = round(time()) + self.novelIndex[tHandle][sTitle]["updated"] = round(time.time()) else: if tHandle in self.noteIndex: if sTitle in self.noteIndex[tHandle]: self.noteIndex[tHandle][sTitle]["synopsis"] = theText - self.noteIndex[tHandle][sTitle]["updated"] = round(time()) + self.noteIndex[tHandle][sTitle]["updated"] = round(time.time()) return def _indexNoteRef(self, tHandle, aLine, nLine, nTitle): diff --git a/tests/reference/proj/1_tagsIndex.json b/tests/reference/proj/1_tagsIndex.json new file mode 100644 index 00000000..2bb3b44a --- /dev/null +++ b/tests/reference/proj/1_tagsIndex.json @@ -0,0 +1,560 @@ +{ + "tagIndex": { + "Bod": [ + 3, + "4c4f28287af27", + "CHARACTER", + "T000001" + ], + "Main": [ + 3, + "2426c6f0ca922", + "PLOT", + "T000001" + ], + "Europe": [ + 3, + "04468803b92e1", + "WORLD", + "T000001" + ] + }, + "refIndex": { + "7a992350f3eb6": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + }, + "8c58a65414c23": { + "T000000": { + "tags": [], + "updated": 123 + } + }, + "88d59a277361b": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + }, + "db7e733775d4d": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + }, + "fb609cd8319dc": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + } + }, + "88243afbe5ed8": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + }, + "T000013": { + "tags": [], + "updated": 123 + } + }, + "f96ec11c6a3da": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + }, + "T000015": { + "tags": [], + "updated": 123 + } + }, + "846352075de7d": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + }, + "441420a886d82": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + } + }, + "eb103bc70c90c": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + } + }, + "f8c0562e50f1b": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + } + }, + "47666c91c7ccf": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [ + [ + 3, + "@pov", + "Bod" + ], + [ + 4, + "@plot", + "Main" + ], + [ + 5, + "@location", + "Europe" + ] + ], + "updated": 123 + } + }, + "4c4f28287af27": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + }, + "2426c6f0ca922": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + }, + "04468803b92e1": { + "T000000": { + "tags": [], + "updated": 123 + }, + "T000001": { + "tags": [], + "updated": 123 + } + } + }, + "novelIndex": { + "7a992350f3eb6": { + "T000001": { + "level": "H1", + "title": "Lorem Ipsum", + "layout": "TITLE", + "synopsis": "", + "cCount": 230, + "wCount": 40, + "pCount": 3, + "updated": 123 + } + }, + "8c58a65414c23": {}, + "88d59a277361b": { + "T000001": { + "level": "H2", + "title": "Prologue", + "layout": "UNNUMBERED", + "synopsis": "Explanation from the lipsum.com website.", + "cCount": 584, + "wCount": 92, + "pCount": 1, + "updated": 123 + } + }, + "db7e733775d4d": { + "T000001": { + "level": "H1", + "title": "Act One", + "layout": "PARTITION", + "synopsis": "", + "cCount": 35, + "wCount": 6, + "pCount": 1, + "updated": 123 + } + }, + "fb609cd8319dc": { + "T000001": { + "level": "H2", + "title": "Chapter One", + "layout": "CHAPTER", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.", + "cCount": 419, + "wCount": 67, + "pCount": 1, + "updated": 123 + } + }, + "88243afbe5ed8": { + "T000001": { + "level": "H3", + "title": "Scene One", + "layout": "SCENE", + "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.", + "cCount": 1197, + "wCount": 174, + "pCount": 2, + "updated": 123 + }, + "T000013": { + "level": "H4", + "title": "Scene One, Section Two", + "layout": "SCENE", + "synopsis": "", + "cCount": 1561, + "wCount": 230, + "pCount": 2, + "updated": 123 + } + }, + "f96ec11c6a3da": { + "T000001": { + "level": "H3", + "title": "Scene Two", + "layout": "SCENE", + "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.", + "cCount": 2034, + "wCount": 299, + "pCount": 3, + "updated": 123 + }, + "T000015": { + "level": "H4", + "title": "Scene Two, Section Two", + "layout": "SCENE", + "synopsis": "", + "cCount": 2009, + "wCount": 301, + "pCount": 3, + "updated": 123 + } + }, + "846352075de7d": { + "T000001": { + "level": "H2", + "title": "Why do we use it?", + "layout": "BOOK", + "synopsis": "", + "cCount": 630, + "wCount": 109, + "pCount": 1, + "updated": 123 + } + }, + "441420a886d82": { + "T000001": { + "level": "H2", + "title": "Chapter Two", + "layout": "CHAPTER", + "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.", + "cCount": 477, + "wCount": 70, + "pCount": 1, + "updated": 123 + } + }, + "eb103bc70c90c": { + "T000001": { + "level": "H3", + "title": "Scene Three", + "layout": "SCENE", + "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.", + "cCount": 3006, + "wCount": 439, + "pCount": 4, + "updated": 123 + } + }, + "f8c0562e50f1b": { + "T000001": { + "level": "H3", + "title": "Scene Four", + "layout": "SCENE", + "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.", + "cCount": 3839, + "wCount": 563, + "pCount": 6, + "updated": 123 + } + }, + "47666c91c7ccf": { + "T000001": { + "level": "H3", + "title": "Scene Five", + "layout": "SCENE", + "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "cCount": 3644, + "wCount": 543, + "pCount": 5, + "updated": 123 + } + } + }, + "noteIndex": { + "4c4f28287af27": { + "T000001": { + "level": "H1", + "title": "Nobody Owens", + "layout": "NOTE", + "synopsis": "", + "cCount": 1864, + "wCount": 284, + "pCount": 3, + "updated": 123 + } + }, + "2426c6f0ca922": { + "T000001": { + "level": "H1", + "title": "Main Plot", + "layout": "NOTE", + "synopsis": "", + "cCount": 1369, + "wCount": 195, + "pCount": 2, + "updated": 123 + } + }, + "04468803b92e1": { + "T000001": { + "level": "H1", + "title": "Ancient Europe", + "layout": "NOTE", + "synopsis": "", + "cCount": 1770, + "wCount": 259, + "pCount": 3, + "updated": 123 + } + } + }, + "textCounts": { + "7a992350f3eb6": [ + 230, + 40, + 3 + ], + "8c58a65414c23": [ + 1058, + 176, + 2 + ], + "88d59a277361b": [ + 584, + 92, + 1 + ], + "db7e733775d4d": [ + 35, + 6, + 1 + ], + "fb609cd8319dc": [ + 419, + 67, + 1 + ], + "88243afbe5ed8": [ + 2758, + 404, + 4 + ], + "f96ec11c6a3da": [ + 4043, + 600, + 6 + ], + "846352075de7d": [ + 630, + 109, + 1 + ], + "441420a886d82": [ + 477, + 70, + 1 + ], + "eb103bc70c90c": [ + 3006, + 439, + 4 + ], + "f8c0562e50f1b": [ + 3839, + 563, + 6 + ], + "47666c91c7ccf": [ + 3644, + 543, + 5 + ], + "4c4f28287af27": [ + 1864, + 284, + 3 + ], + "2426c6f0ca922": [ + 1369, + 195, + 2 + ], + "04468803b92e1": [ + 1770, + 259, + 3 + ] + } +} \ No newline at end of file diff --git a/tests/test_index.py b/tests/test_index.py index 084a9d39..11e6c595 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -3,11 +3,143 @@ """ import pytest +import time +import os +import json + +from shutil import copyfile + +from nwtools import cmpFiles from nw.core.project import NWProject from nw.core.index import NWIndex from nw.constants import nwItemClass, nwItemLayout +@pytest.mark.project +def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): + """Test core functionality of scaning, saving, loading and checking + the index cache file. + """ + projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json") + testFile = os.path.join(nwTempProj, "1_tagsIndex.json") + refFile = os.path.join(nwRef, "proj", "1_tagsIndex.json") + + theProject = NWProject(nwDummy) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwLipsum) + + theProject.mainConf.debugInfo = True + monkeypatch.setattr(time, "time", lambda: 123.4) + + theIndex = NWIndex(theProject, nwDummy) + notIndexable = { + "b3643d0f92e32": False, # Novel ROOT + "45e6b01ca35c1": False, # Chapter One FOLDER + "6bd935d2490cd": False, # Chapter Two FOLDER + "67a8707f2f249": False, # Character ROOT + "6c6afb1247750": False, # Plot ROOT + "60bdf227455cc": False, # World ROOT + } + for tItem in theProject.projTree: + assert theIndex.reIndexHandle(tItem.itemHandle) is notIndexable.get(tItem.itemHandle, True) + + assert not theIndex.reIndexHandle(None) + + # Dummy exception function + def doPanic(*arg, **kwargs): + raise Exception + + # Make the save fail + monkeypatch.setattr(json, "dumps", doPanic) + assert not theIndex.saveIndex() + + # Make the save pass + monkeypatch.undo() + assert theIndex.saveIndex() + + # Take a copy of the index + tagIndex = str(theIndex.tagIndex) + refIndex = str(theIndex.refIndex) + novelIndex = str(theIndex.novelIndex) + noteIndex = str(theIndex.noteIndex) + textCounts = str(theIndex.textCounts) + + # Delete a handle + assert theIndex.tagIndex.get("Bod", None) is not None + assert theIndex.refIndex.get("4c4f28287af27", None) is not None + assert theIndex.noteIndex.get("4c4f28287af27", None) is not None + assert theIndex.textCounts.get("4c4f28287af27", None) is not None + theIndex.deleteHandle("4c4f28287af27") + assert theIndex.tagIndex.get("Bod", None) is None + assert theIndex.refIndex.get("4c4f28287af27", None) is None + assert theIndex.noteIndex.get("4c4f28287af27", None) is None + assert theIndex.textCounts.get("4c4f28287af27", None) is None + + # Clear the index + theIndex.clearIndex() + assert not theIndex.tagIndex + assert not theIndex.refIndex + assert not theIndex.novelIndex + assert not theIndex.noteIndex + assert not theIndex.textCounts + + # Make the load fail + monkeypatch.setattr(json, "loads", doPanic) + assert not theIndex.loadIndex() + + # Make the load pass + monkeypatch.undo() + assert theIndex.loadIndex() + + assert str(theIndex.tagIndex) == tagIndex + assert str(theIndex.refIndex) == refIndex + assert str(theIndex.novelIndex) == novelIndex + assert str(theIndex.noteIndex) == noteIndex + assert str(theIndex.textCounts) == textCounts + + # Break the index and check that we notice + assert not theIndex.indexBroken + theIndex.tagIndex["Bod"].append("Stuff") # No longer len() == 4 + theIndex.checkIndex() + assert theIndex.indexBroken + + assert theIndex.loadIndex() + assert not theIndex.indexBroken + theIndex.refIndex["fb609cd8319dc"]["T000001"]["tags"].append("Stuff") # No longer len() == 3 + theIndex.checkIndex() + assert theIndex.indexBroken + + assert theIndex.loadIndex() + assert not theIndex.indexBroken + theIndex.novelIndex["7a992350f3eb6"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8 + theIndex.checkIndex() + assert theIndex.indexBroken + + assert theIndex.loadIndex() + assert not theIndex.indexBroken + theIndex.noteIndex["4c4f28287af27"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8 + theIndex.checkIndex() + assert theIndex.indexBroken + + assert theIndex.loadIndex() + assert not theIndex.indexBroken + theIndex.textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3 + theIndex.checkIndex() + assert theIndex.indexBroken + + # Make the try/except trigger as well + assert theIndex.loadIndex() + assert not theIndex.indexBroken + theIndex.refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name + theIndex.checkIndex() + assert theIndex.indexBroken + + # Finalise + assert theProject.closeProject() + + copyfile(projFile, testFile) + assert cmpFiles(testFile, refFile) + @pytest.mark.project def testIndexScanThis(nwMinimal, nwDummy): """Test the tag scanner function scanThis. From 1a70a4ec1c7b4c014de9e9d988be74293f715b3b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Sep 2020 23:10:47 +0200 Subject: [PATCH 40/51] Get that one last line in the About dialog test --- tests/test_dialogs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index a95eba02..defc6397 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -380,6 +380,10 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp): assert msgAbout.pageAbout.document().characterCount() > 100 assert msgAbout.pageLicense.document().characterCount() > 100 + msgAbout.mainConf.guiLang = "whatever" + msgAbout._fillLicensePage() + assert msgAbout.pageLicense.toPlainText() == "Error loading license text ..." + # Qt About monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None) nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger) From 238b3c254e7e5c5f712c6d0ab80736944ae89479 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 30 Sep 2020 23:43:50 +0200 Subject: [PATCH 41/51] Fixed the monkeypatching of time.time() --- nw/core/index.py | 21 +++++++++++---------- tests/test_index.py | 3 +-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 13c6c092..8925b07e 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -29,7 +29,8 @@ import nw import logging import json import os -import time + +from time import time from nw.constants import ( nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert @@ -176,7 +177,7 @@ class NWIndex(): if "textCounts" in theData.keys(): self.textCounts = theData["textCounts"] - nowTime = round(time.time()) + nowTime = round(time()) self.timeNovel = nowTime self.timeNote = nowTime self.timeIndex = nowTime @@ -304,7 +305,7 @@ class NWIndex(): self.refIndex[tHandle] = {} self.refIndex[tHandle]["T000000"] = { "tags" : [], - "updated" : round(time.time()), + "updated" : round(time()), } if itemLayout == nwItemLayout.NOTE: self.noteIndex[tHandle] = {} @@ -359,7 +360,7 @@ class NWIndex(): self._indexWordCounts(tHandle, isNovel, lastText, nTitle) # Update timestamps for index changes - nowTime = round(time.time()) + nowTime = round(time()) self.timeIndex = nowTime if isNovel: self.timeNovel = nowTime @@ -394,7 +395,7 @@ class NWIndex(): sTitle = "T%06d" % nLine self.refIndex[tHandle][sTitle] = { "tags" : [], - "updated" : round(time.time()), + "updated" : round(time()), } theData = { "level" : hDepth, @@ -404,7 +405,7 @@ class NWIndex(): "cCount" : 0, "wCount" : 0, "pCount" : 0, - "updated" : round(time.time()), + "updated" : round(time()), } if hText != "": @@ -428,14 +429,14 @@ class NWIndex(): self.novelIndex[tHandle][sTitle]["cCount"] = cC self.novelIndex[tHandle][sTitle]["wCount"] = wC self.novelIndex[tHandle][sTitle]["pCount"] = pC - self.novelIndex[tHandle][sTitle]["updated"] = round(time.time()) + self.novelIndex[tHandle][sTitle]["updated"] = round(time()) else: if tHandle in self.noteIndex: if sTitle in self.noteIndex[tHandle]: self.noteIndex[tHandle][sTitle]["cCount"] = cC self.noteIndex[tHandle][sTitle]["wCount"] = wC self.noteIndex[tHandle][sTitle]["pCount"] = pC - self.noteIndex[tHandle][sTitle]["updated"] = round(time.time()) + self.noteIndex[tHandle][sTitle]["updated"] = round(time()) return def _indexSynopsis(self, tHandle, isNovel, theText, nTitle): @@ -446,12 +447,12 @@ class NWIndex(): if tHandle in self.novelIndex: if sTitle in self.novelIndex[tHandle]: self.novelIndex[tHandle][sTitle]["synopsis"] = theText - self.novelIndex[tHandle][sTitle]["updated"] = round(time.time()) + self.novelIndex[tHandle][sTitle]["updated"] = round(time()) else: if tHandle in self.noteIndex: if sTitle in self.noteIndex[tHandle]: self.noteIndex[tHandle][sTitle]["synopsis"] = theText - self.noteIndex[tHandle][sTitle]["updated"] = round(time.time()) + self.noteIndex[tHandle][sTitle]["updated"] = round(time()) return def _indexNoteRef(self, tHandle, aLine, nLine, nTitle): diff --git a/tests/test_index.py b/tests/test_index.py index 11e6c595..a86fb017 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -3,7 +3,6 @@ """ import pytest -import time import os import json @@ -29,7 +28,7 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): assert theProject.openProject(nwLipsum) theProject.mainConf.debugInfo = True - monkeypatch.setattr(time, "time", lambda: 123.4) + monkeypatch.setattr("nw.core.index.time", lambda: 123.4) theIndex = NWIndex(theProject, nwDummy) notIndexable = { From ff0ee1f0201e88a42b70d28daccf625778b6493b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 14:03:50 +0200 Subject: [PATCH 42/51] Add mac test --- .github/workflows/pytest_mac_3_8.yml | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/pytest_mac_3_8.yml diff --git a/.github/workflows/pytest_mac_3_8.yml b/.github/workflows/pytest_mac_3_8.yml new file mode 100644 index 00000000..41ac19ac --- /dev/null +++ b/.github/workflows/pytest_mac_3_8.yml @@ -0,0 +1,32 @@ +name: macOS (3.8) + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + pyTestMac38: + runs-on: macos-latest + steps: + - name: Python Setup + uses: actions/setup-python@v1 + with: + python-version: 3.8 + architecture: x64 + - name: Install Packages + run: | + brew install enchant + - name: Checkout Source + uses: actions/checkout@v2 + - name: Install Dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt + pip install pytest-timeout + pip install pytest-qt + - name: Run Tests + run: | + export QT_QPA_PLATFORM=offscreen + pytest -v --timeout=60 From 0179965a8d70f74fee43fec50363b6327d19e88c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 14:31:05 +0200 Subject: [PATCH 43/51] Improve project wizard test --- tests/test_dialogs.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index defc6397..f5e7da10 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -733,6 +733,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {"projPath": nwMinimal}) assert not nwGUI.newProject() + monkeypatch.undo() nwGUI.closeMain() nwGUI.close() @@ -747,6 +748,8 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) + + monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *args: None) nwGUI.mainConf.lastPath = " " for wStep in range(4): @@ -754,7 +757,12 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): # dictionary that defines it. # The Wizard - nwWiz = GuiProjectWizard(nwGUI) + nwGUI.closeProject() + nwGUI.showNewProjectDialog() + qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000) + + nwWiz = getGuiItem("GuiProjectWizard") + assert isinstance(nwWiz, GuiProjectWizard) nwWiz.show() qtbot.waitForWindowShown(nwWiz) @@ -764,7 +772,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): assert not nwWiz.button(QWizard.NextButton).isEnabled() qtbot.wait(stepDelay) - for c in "Test Minimal": + for c in ("Test Minimal %d" % wStep): qtbot.keyClick(introPage.projName, c, delay=typeDelay) qtbot.wait(stepDelay) @@ -798,7 +806,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): qtbot.wait(stepDelay) qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) - projPath = os.path.join(nwMinimal, "Test Minimal") + projPath = os.path.join(nwMinimal, "Test Minimal %d" % wStep) assert storagePage.projPath.text() == projPath # Setting projPath should activate the button @@ -850,11 +858,10 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): finalPage = nwWiz.currentPage() assert isinstance(finalPage, ProjWizardFinalPage) assert nwWiz.button(QWizard.FinishButton).isEnabled() - qtbot.mouseClick(nwWiz.button(QWizard.FinishButton), Qt.LeftButton) # Check Data projData = nwGUI._assembleProjectWizardData(nwWiz) - assert projData["projName"] == "Test Minimal" + assert projData["projName"] == "Test Minimal %d" % wStep assert projData["projTitle"] == "Minimal Novel" assert projData["projAuthors"] == "Jane Doe" assert projData["projPath"] == projPath @@ -884,6 +891,10 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): assert projData["numScenes"] == 0 assert not projData["chFolders"] + nwWiz.reject() + nwWiz.close() + del nwWiz + # qtbot.stopForInteraction() nwGUI.closeMain() nwGUI.close() From 26ebcd496d9b7fef51b64c7fc9ab65f815d704d8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 20:34:48 +0200 Subject: [PATCH 44/51] Improve itemEditor test, and refactor actions --- .github/workflows/syntax.yml | 2 +- .../{pytest_3_6.yml => test_linux_3.6.yml} | 12 +++---- .../{pytest_3_7.yml => test_linux_3.7.yml} | 12 +++---- ...{pytest_3_8_cov.yml => test_linux_3.8.yml} | 12 +++---- .../{pytest_mac_3_8.yml => test_mac.yml} | 6 +++- .github/workflows/test_win.yml | 32 +++++++++++++++++++ nw/gui/docmerge.py | 2 +- nw/gui/docsplit.py | 4 +-- nw/gui/projtree.py | 4 +-- nw/guimain.py | 15 ++++----- tests/reference/gui/3_nwProject.nwx | 4 +-- tests/test_dialogs.py | 24 +++++++++++--- tests/test_gui.py | 2 ++ 13 files changed, 91 insertions(+), 40 deletions(-) rename .github/workflows/{pytest_3_6.yml => test_linux_3.6.yml} (75%) rename .github/workflows/{pytest_3_7.yml => test_linux_3.7.yml} (75%) rename .github/workflows/{pytest_3_8_cov.yml => test_linux_3.8.yml} (77%) rename .github/workflows/{pytest_mac_3_8.yml => test_mac.yml} (83%) create mode 100644 .github/workflows/test_win.yml diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index f697c4ec..038480f7 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -13,7 +13,7 @@ jobs: - name: Python Setup uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3 architecture: x64 - name: Checkout Source uses: actions/checkout@v2 diff --git a/.github/workflows/pytest_3_6.yml b/.github/workflows/test_linux_3.6.yml similarity index 75% rename from .github/workflows/pytest_3_6.yml rename to .github/workflows/test_linux_3.6.yml index 058a1281..20398b22 100644 --- a/.github/workflows/pytest_3_6.yml +++ b/.github/workflows/test_linux_3.6.yml @@ -1,4 +1,4 @@ -name: python 3.6 +name: Linux (3.6) on: push: @@ -7,7 +7,7 @@ on: branches: [ main, dev ] jobs: - pyTest36: + testLinux36: runs-on: ubuntu-latest steps: - name: Python Setup @@ -18,16 +18,16 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default + sudo apt install libenchant-dev qt5-default - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies run: | pip install --upgrade pip pip install -r requirements.txt - pip install PyVirtualDisplay pip install pytest-timeout - pip install pytest-xvfb pip install pytest-qt - name: Run Tests - run: xvfb-run pytest -v --timeout=60 + run: | + export QT_QPA_PLATFORM=offscreen + pytest -v --timeout=60 diff --git a/.github/workflows/pytest_3_7.yml b/.github/workflows/test_linux_3.7.yml similarity index 75% rename from .github/workflows/pytest_3_7.yml rename to .github/workflows/test_linux_3.7.yml index 4de6d2aa..5652a626 100644 --- a/.github/workflows/pytest_3_7.yml +++ b/.github/workflows/test_linux_3.7.yml @@ -1,4 +1,4 @@ -name: python 3.7 +name: Linux (3.7) on: push: @@ -7,7 +7,7 @@ on: branches: [ main, dev ] jobs: - pyTest37: + testLinux37: runs-on: ubuntu-latest steps: - name: Python Setup @@ -18,16 +18,16 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default + sudo apt install libenchant-dev qt5-default - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies run: | pip install --upgrade pip pip install -r requirements.txt - pip install PyVirtualDisplay pip install pytest-timeout - pip install pytest-xvfb pip install pytest-qt - name: Run Tests - run: xvfb-run pytest -v --timeout=60 + run: | + export QT_QPA_PLATFORM=offscreen + pytest -v --timeout=60 diff --git a/.github/workflows/pytest_3_8_cov.yml b/.github/workflows/test_linux_3.8.yml similarity index 77% rename from .github/workflows/pytest_3_8_cov.yml rename to .github/workflows/test_linux_3.8.yml index 547c6938..502de464 100644 --- a/.github/workflows/pytest_3_8_cov.yml +++ b/.github/workflows/test_linux_3.8.yml @@ -1,4 +1,4 @@ -name: python 3.8 +name: Linux (3.8) on: push: @@ -7,7 +7,7 @@ on: branches: [ main, dev ] jobs: - pyTest38Cov: + testLinux38: runs-on: ubuntu-latest steps: - name: Python Setup @@ -18,20 +18,20 @@ jobs: - name: Install Packages run: | sudo apt update - sudo apt install xvfb libenchant-dev qt5-default + sudo apt install libenchant-dev qt5-default - name: Checkout Source uses: actions/checkout@v2 - name: Install Dependencies run: | pip install --upgrade pip pip install -r requirements.txt - pip install PyVirtualDisplay pip install pytest-timeout pip install pytest-cov - pip install pytest-xvfb pip install pytest-qt pip install codecov - name: Run Tests - run: xvfb-run pytest -v --cov=nw --timeout=60 + run: | + export QT_QPA_PLATFORM=offscreen + pytest -v --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/.github/workflows/pytest_mac_3_8.yml b/.github/workflows/test_mac.yml similarity index 83% rename from .github/workflows/pytest_mac_3_8.yml rename to .github/workflows/test_mac.yml index 41ac19ac..0478bf71 100644 --- a/.github/workflows/pytest_mac_3_8.yml +++ b/.github/workflows/test_mac.yml @@ -7,7 +7,7 @@ on: branches: [ main, dev ] jobs: - pyTestMac38: + testMac38: runs-on: macos-latest steps: - name: Python Setup @@ -25,8 +25,12 @@ jobs: pip install --upgrade pip pip install -r requirements.txt pip install pytest-timeout + pip install pytest-cov pip install pytest-qt + pip install codecov - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen pytest -v --timeout=60 + - name: Upload to Codecov + uses: codecov/codecov-action@v1 diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml new file mode 100644 index 00000000..aa9ef284 --- /dev/null +++ b/.github/workflows/test_win.yml @@ -0,0 +1,32 @@ +name: Windows (3.8) + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + testWin38: + runs-on: windows-latest + steps: + - name: Python Setup + uses: actions/setup-python@v1 + with: + python-version: 3.8 + architecture: x64 + - name: Checkout Source + uses: actions/checkout@v2 + - name: Install Dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt + pip install pytest-timeout + pip install pytest-cov + pip install pytest-qt + pip install codecov + - name: Run Tests + run: | + pytest -v --timeout=60 + - name: Upload to Codecov + uses: codecov/codecov-action@v1 diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py index 786eb429..2418a161 100644 --- a/nw/gui/docmerge.py +++ b/nw/gui/docmerge.py @@ -133,7 +133,7 @@ class GuiDocMerge(QDialog): theDoc.openDocument(nHandle, False) theDoc.saveDocument(theText) - self.theParent.treeView.revealTreeItem(nHandle) + self.theParent.treeView.revealNewTreeItem(nHandle) self.theParent.openDocument(nHandle, doScroll=True) self._doClose() diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py index 836fd5cb..3f497c18 100644 --- a/nw/gui/docsplit.py +++ b/nw/gui/docsplit.py @@ -178,7 +178,7 @@ class GuiDocSplit(QDialog): fHandle = self.theProject.newFolder( srcItem.itemName, srcItem.itemClass, srcItem.parHandle ) - self.theParent.treeView.revealTreeItem(fHandle) + self.theParent.treeView.revealNewTreeItem(fHandle) logger.verbose("Creating folder %s" % fHandle) # Loop through, and create the files @@ -213,7 +213,7 @@ class GuiDocSplit(QDialog): theDoc.openDocument(nHandle, False) theDoc.saveDocument(theText) theDoc.clearDocument() - self.theParent.treeView.revealTreeItem(nHandle) + self.theParent.treeView.revealNewTreeItem(nHandle) self._doClose() diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 65c4bb1f..dfada052 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -240,12 +240,12 @@ class GuiProjectTree(QTreeWidget): # Add the new item to the tree if tHandle is not None: - self.revealTreeItem(tHandle, nHandle) + self.revealNewTreeItem(tHandle, nHandle) self.theParent.editItem(tHandle) return True - def revealTreeItem(self, tHandle, nHandle=None): + def revealNewTreeItem(self, tHandle, nHandle=None): """Reveal a newly added project item in the project tree. """ nwItem = self.theProject.projTree[tHandle] diff --git a/nw/guimain.py b/nw/guimain.py index bfb02bd5..5f237bb7 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -659,14 +659,13 @@ class GuiMain(QMainWindow): return logger.verbose("Requesting change to item %s" % tHandle) - if self.mainConf.showGUI: - dlgProj = GuiItemEditor(self, self.theProject, tHandle) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: - self.treeView.setTreeItemValues(tHandle) - self.treeMeta.updateViewBox(tHandle) - self.docEditor.updateDocInfo(tHandle) - self.docViewer.updateDocInfo(tHandle) + dlgProj = GuiItemEditor(self, self.theProject, tHandle) + dlgProj.exec_() + if dlgProj.result() == QDialog.Accepted: + self.treeView.setTreeItemValues(tHandle) + self.treeMeta.updateViewBox(tHandle) + self.docEditor.updateDocInfo(tHandle) + self.docViewer.updateDocInfo(tHandle) return diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index cb018a6a..7bcf7553 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -44,7 +44,7 @@ ROOT NOVEL New - False + True
Title Page @@ -63,7 +63,7 @@ FOLDER NOVEL New - False + True New Chapter diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index f5e7da10..a71653ee 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -139,7 +139,7 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR nwGUI.closeMain() @pytest.mark.gui -def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) qtbot.addWidget(nwGUI) nwGUI.show() @@ -150,8 +150,16 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": nwFuncTemp}) assert nwGUI.openDocument("0e17daca5f3e1") + assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True) + + monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None) + nwGUI.mainMenu.aEditItem.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) + + itemEdit = getGuiItem("GuiItemEditor") + assert isinstance(itemEdit, GuiItemEditor) + itemEdit.show() - itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1") qtbot.addWidget(itemEdit) assert itemEdit.editName.text() == "New Scene" @@ -168,7 +176,13 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert not itemEdit.editExport.isChecked() itemEdit._doSave() - itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1") + nwGUI.mainMenu.aEditItem.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) + + itemEdit = getGuiItem("GuiItemEditor") + assert isinstance(itemEdit, GuiItemEditor) + itemEdit.show() + qtbot.addWidget(itemEdit) assert itemEdit.editName.text() == "Just a Page" assert itemEdit.editStatus.currentData() == "Note" @@ -194,8 +208,8 @@ def testItemEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - nwGUI.closeMain() # qtbot.stopForInteraction() + nwGUI.closeMain() @pytest.mark.gui def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): @@ -857,7 +871,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): # Final Page finalPage = nwWiz.currentPage() assert isinstance(finalPage, ProjWizardFinalPage) - assert nwWiz.button(QWizard.FinishButton).isEnabled() + assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it # Check Data projData = nwGUI._assembleProjectWizardData(nwWiz) diff --git a/tests/test_gui.py b/tests/test_gui.py index 0cd9dd87..3c9221e8 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -64,6 +64,8 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert os.path.isfile(logFile) + nwGUI.closeMain() + nwGUI.close() nwGUI = nw.main( ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] From f1d8265a02ae412d27b9ec3695c5a738387d2366 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 20:44:52 +0200 Subject: [PATCH 45/51] A couple of fixes to tests breaking on windows and mac --- nw/__init__.py | 2 +- tests/test_dialogs.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index eca0e5ee..8419dd81 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -203,7 +203,7 @@ def main(sysArgs=None): if not logFile == "" and toFile: if os.path.isfile(logFile+".bak"): - os.remove(logFile+".bak") + os.unlink(logFile+".bak") if os.path.isfile(logFile): os.rename(logFile, logFile+".bak") diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index a71653ee..9fa4c531 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -540,12 +540,16 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): # We assume the export itself by the Qt library works, so we just # check that novelWriter successfully writes the files. assert nwBuild._saveDocument(nwBuild.FMT_ODT) - assert nwBuild._saveDocument(nwBuild.FMT_PDF) - assert nwBuild._saveDocument(nwBuild.FMT_MD) - assert nwBuild._saveDocument(nwBuild.FMT_TXT) assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.odt")) - assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf")) + + if not nwGUI.mainConf.osDarwin: + assert nwBuild._saveDocument(nwBuild.FMT_PDF) + assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf")) + + assert nwBuild._saveDocument(nwBuild.FMT_MD) assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.md")) + + assert nwBuild._saveDocument(nwBuild.FMT_TXT) assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.txt")) # Close the build tool From d63c741cfe98c5b267e58b321f9bbd53b765c545 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 21:00:37 +0200 Subject: [PATCH 46/51] Remove logfile related launch options --- nw/__init__.py | 35 +++++------------------------------ tests/test_gui.py | 25 +++---------------------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 8419dd81..199d20ca 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -96,15 +96,13 @@ def main(sysArgs=None): sysArgs = sys.argv[1:] # Valid Input Options - shortOpt = "hvq" + shortOpt = "hv" longOpt = [ "help", "version", "info", "debug", "verbose", - "quiet", - "logfile=", "style=", "config=", "data=", @@ -126,8 +124,6 @@ def main(sysArgs=None): " --info Print additional runtime information.\n" " --debug Print debug output. Includes --info.\n" " --verbose Increase verbosity of debug output. Includes --debug.\n" - " -q, --quiet Disable output to command line. Does not affect log file.\n" - " --logfile= Specify log file.\n" " --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n" " --config= Alternative config file.\n" " --data= Alternative user data path.\n" @@ -142,9 +138,6 @@ def main(sysArgs=None): # Defaults debugLevel = logging.WARN logFormat = "{levelname:8} {message:}" - logFile = "" - toFile = False - toStd = True confPath = None dataPath = None testMode = False @@ -176,11 +169,6 @@ def main(sysArgs=None): elif inOpt == "--debug": debugLevel = logging.DEBUG logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}" - elif inOpt == "--logfile": - logFile = inArg - toFile = True - elif inOpt in ("-q", "--quiet"): - toStd = False elif inOpt == "--verbose": debugLevel = VERBOSE logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}" @@ -200,23 +188,10 @@ def main(sysArgs=None): # Set Logging logFmt = logging.Formatter(fmt=logFormat, style="{") - - if not logFile == "" and toFile: - if os.path.isfile(logFile+".bak"): - os.unlink(logFile+".bak") - if os.path.isfile(logFile): - os.rename(logFile, logFile+".bak") - - fHandle = logging.FileHandler(logFile) - fHandle.setLevel(debugLevel) - fHandle.setFormatter(logFmt) - logger.addHandler(fHandle) - - if toStd: - cHandle = logging.StreamHandler() - cHandle.setLevel(debugLevel) - cHandle.setFormatter(logFmt) - logger.addHandler(cHandle) + cHandle = logging.StreamHandler() + cHandle.setLevel(debugLevel) + cHandle.setFormatter(logFmt) + logger.addHandler(cHandle) logger.setLevel(debugLevel) logger.info("Starting novelWriter %s (%s) %s" % ( diff --git a/tests/test_gui.py b/tests/test_gui.py index 3c9221e8..0334cf2d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -36,45 +36,26 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): nwGUI.close() nwGUI = nw.main( - ["--testmode", "--info", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--info", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert nw.logger.getEffectiveLevel() == logging.INFO nwGUI.closeMain() nwGUI.close() nwGUI = nw.main( - ["--testmode", "--debug", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--debug", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert nw.logger.getEffectiveLevel() == logging.DEBUG nwGUI.closeMain() nwGUI.close() nwGUI = nw.main( - ["--testmode", "--verbose", "--quiet", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--verbose", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) assert nw.logger.getEffectiveLevel() == 5 nwGUI.closeMain() nwGUI.close() - # Log file - logFile = os.path.join(nwTemp, "logFile.log") - bakFile = os.path.join(nwTemp, "logFile.log.bak") - - nwGUI = nw.main( - ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] - ) - assert os.path.isfile(logFile) - nwGUI.closeMain() - nwGUI.close() - - nwGUI = nw.main( - ["--testmode", "--logfile=%s" % logFile, "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] - ) - assert os.path.isfile(bakFile) - assert os.path.isfile(logFile) - nwGUI.closeMain() - nwGUI.close() - # Other options with pytest.raises(SystemExit): nwGUI = nw.main( From 4b1c45ddb453d8a74f813b30060950b6c72b39cd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 22:01:41 +0200 Subject: [PATCH 47/51] Fixed coverage in actions and updated launch test --- .github/workflows/test_linux_3.8.yml | 2 +- .github/workflows/test_mac.yml | 2 +- .github/workflows/test_win.yml | 2 +- nw/__init__.py | 32 +++++++++++----------- tests/test_gui.py | 40 +++++++++++++++++++++++----- 5 files changed, 53 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test_linux_3.8.yml b/.github/workflows/test_linux_3.8.yml index 502de464..dd27ce70 100644 --- a/.github/workflows/test_linux_3.8.yml +++ b/.github/workflows/test_linux_3.8.yml @@ -32,6 +32,6 @@ jobs: - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen - pytest -v --timeout=60 + pytest -v --cov-nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/.github/workflows/test_mac.yml b/.github/workflows/test_mac.yml index 0478bf71..d409f81e 100644 --- a/.github/workflows/test_mac.yml +++ b/.github/workflows/test_mac.yml @@ -31,6 +31,6 @@ jobs: - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen - pytest -v --timeout=60 + pytest -v --cov-nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml index aa9ef284..ebdee693 100644 --- a/.github/workflows/test_win.yml +++ b/.github/workflows/test_win.yml @@ -27,6 +27,6 @@ jobs: pip install codecov - name: Run Tests run: | - pytest -v --timeout=60 + pytest -v --cov-nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/nw/__init__.py b/nw/__init__.py index 199d20ca..aa755a2e 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -28,7 +28,6 @@ import sys import getopt import logging -import os from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage @@ -158,12 +157,12 @@ def main(sysArgs=None): for inOpt, inArg in inOpts: if inOpt in ("-h", "--help"): print(helpMsg) - sys.exit() + sys.exit(0) elif inOpt in ("-v", "--version"): print("novelWriter %s Version %s [%s]" % ( __status__, __version__, __date__) ) - sys.exit() + sys.exit(0) elif inOpt == "--info": debugLevel = logging.INFO elif inOpt == "--debug": @@ -224,19 +223,20 @@ def main(sysArgs=None): errorData.append("Python module 'lxml' is missing.") if errorData: - errApp = QApplication([]) - errMsg = QErrorMessage() - errMsg.resize(500, 300) - errMsg.showMessage(( - "

A critical error has been encountered

" - "

novelWriter cannot start due to the following issues:

" - "

 - %s

" - "

Shutting down ...

" - ) % ( - "
 - ".join(errorData) - )) - errApp.exec_() - sys.exit(1) + if not testMode: + errApp = QApplication([]) + errMsg = QErrorMessage() + errMsg.resize(500, 300) + errMsg.showMessage(( + "

A critical error has been encountered

" + "

novelWriter cannot start due to the following issues:

" + "

 - %s

" + "

Shutting down ...

" + ) % ( + "
 - ".join(errorData) + )) + errApp.exec_() + sys.exit(10 + len(errorData)) # Finish initialising config CONFIG.initConfig(confPath, dataPath) diff --git a/tests/test_gui.py b/tests/test_gui.py index 0334cf2d..3bffd372 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -6,6 +6,7 @@ import nw import pytest import logging import os +import sys from shutil import copyfile from nwtools import cmpFiles @@ -25,16 +26,17 @@ typeDelay = 1 stepDelay = 20 @pytest.mark.gui -def testLaunch(qtbot, nwFuncTemp, nwTemp): +def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): - # Log Levels + # Defaults nwGUI = nw.main( - ["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp, "--style=Fusion"] ) assert nw.logger.getEffectiveLevel() == logging.WARNING nwGUI.closeMain() nwGUI.close() + # Log Levels nwGUI = nw.main( ["--testmode", "--info", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) @@ -56,20 +58,46 @@ def testLaunch(qtbot, nwFuncTemp, nwTemp): nwGUI.closeMain() nwGUI.close() - # Other options - with pytest.raises(SystemExit): + # Help and Version + with pytest.raises(SystemExit) as ex: nwGUI = nw.main( ["--testmode", "--help", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) nwGUI.closeMain() nwGUI.close() + assert ex.value.code == 0 - with pytest.raises(SystemExit): + with pytest.raises(SystemExit) as ex: + nwGUI = nw.main( + ["--testmode", "--version", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + nwGUI.closeMain() + nwGUI.close() + assert ex.value.code == 0 + + # Invalid options + with pytest.raises(SystemExit) as ex: nwGUI = nw.main( ["--testmode", "--invalid", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] ) nwGUI.closeMain() nwGUI.close() + assert ex.value.code == 2 + + # Simulate import error + monkeypatch.setitem(sys.modules, "PyQt5.QtSvg", None) + monkeypatch.setitem(sys.modules, "lxml", None) + monkeypatch.setattr("sys.hexversion", 0x0) + monkeypatch.setattr("nw.CONFIG.verQtValue", 50000) + monkeypatch.setattr("nw.CONFIG.verPyQtValue", 50000) + with pytest.raises(SystemExit) as ex: + nwGUI = nw.main( + ["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ) + nwGUI.closeMain() + nwGUI.close() + assert ex.value.code == 15 + monkeypatch.undo() @pytest.mark.gui def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): From 401b9df57f0563ac0fb9fdf546fd46ad397f519b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 22:05:25 +0200 Subject: [PATCH 48/51] Typo in coverage switch --- .github/workflows/test_linux_3.8.yml | 2 +- .github/workflows/test_mac.yml | 2 +- .github/workflows/test_win.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_linux_3.8.yml b/.github/workflows/test_linux_3.8.yml index dd27ce70..a1e2a0e0 100644 --- a/.github/workflows/test_linux_3.8.yml +++ b/.github/workflows/test_linux_3.8.yml @@ -32,6 +32,6 @@ jobs: - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen - pytest -v --cov-nw --timeout=60 + pytest -v --cov=nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/.github/workflows/test_mac.yml b/.github/workflows/test_mac.yml index d409f81e..0a99a85a 100644 --- a/.github/workflows/test_mac.yml +++ b/.github/workflows/test_mac.yml @@ -31,6 +31,6 @@ jobs: - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen - pytest -v --cov-nw --timeout=60 + pytest -v --cov=nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml index ebdee693..d79a931e 100644 --- a/.github/workflows/test_win.yml +++ b/.github/workflows/test_win.yml @@ -27,6 +27,6 @@ jobs: pip install codecov - name: Run Tests run: | - pytest -v --cov-nw --timeout=60 + pytest -v --cov=nw --timeout=60 - name: Upload to Codecov uses: codecov/codecov-action@v1 From 79620ebc19cae310c7639e76390dbd96a9538295 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 22:22:58 +0200 Subject: [PATCH 49/51] Attempt to fix the wizard test --- tests/test_dialogs.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 9fa4c531..5afbaa4b 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -752,21 +752,11 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): assert not nwGUI.newProject() monkeypatch.undo() - nwGUI.closeMain() - nwGUI.close() - - # qtbot.stopForInteraction() ## # Test the Wizard ## - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) - qtbot.addWidget(nwGUI) - nwGUI.show() - qtbot.waitForWindowShown(nwGUI) - qtbot.wait(stepDelay) - monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *args: None) nwGUI.mainConf.lastPath = " " @@ -782,7 +772,8 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwWiz = getGuiItem("GuiProjectWizard") assert isinstance(nwWiz, GuiProjectWizard) nwWiz.show() - qtbot.waitForWindowShown(nwWiz) + nwWiz.setObjectName("Dummy%d" % wStep) # Hack to prevent returning the same object twice + qtbot.wait(stepDelay) # Intro Page introPage = nwWiz.currentPage() @@ -911,7 +902,6 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwWiz.reject() nwWiz.close() - del nwWiz # qtbot.stopForInteraction() nwGUI.closeMain() From a460c3d6825ee712c8e49eb207f4cf7e44553364 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 23:09:21 +0200 Subject: [PATCH 50/51] Use the same wizard dialog for all four tests --- tests/test_dialogs.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 5afbaa4b..2c2a7efc 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -760,21 +760,19 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *args: None) nwGUI.mainConf.lastPath = " " + nwGUI.closeProject() + nwGUI.showNewProjectDialog() + qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000) + + nwWiz = getGuiItem("GuiProjectWizard") + assert isinstance(nwWiz, GuiProjectWizard) + nwWiz.show() + qtbot.wait(stepDelay) + for wStep in range(4): # This does not actually create the project, it just generates the # dictionary that defines it. - # The Wizard - nwGUI.closeProject() - nwGUI.showNewProjectDialog() - qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000) - - nwWiz = getGuiItem("GuiProjectWizard") - assert isinstance(nwWiz, GuiProjectWizard) - nwWiz.show() - nwWiz.setObjectName("Dummy%d" % wStep) # Hack to prevent returning the same object twice - qtbot.wait(stepDelay) - # Intro Page introPage = nwWiz.currentPage() assert isinstance(introPage, ProjWizardIntroPage) @@ -900,8 +898,11 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): assert projData["numScenes"] == 0 assert not projData["chFolders"] - nwWiz.reject() - nwWiz.close() + # Restart the wizard for next iteration + nwWiz.restart() + + nwWiz.reject() + nwWiz.close() # qtbot.stopForInteraction() nwGUI.closeMain() From 1bf5c2de91380b94755a5bc1de0fd2dcdb89cf53 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Oct 2020 23:17:43 +0200 Subject: [PATCH 51/51] Just disable the wizard test for macOS --- tests/test_dialogs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 2c2a7efc..826db2db 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -6,6 +6,7 @@ import nw import pytest import json import os +import sys from shutil import copyfile from nwtools import cmpFiles, getGuiItem @@ -718,6 +719,10 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef @pytest.mark.gui def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): + if sys.platform.startswith("darwin"): + # Disable for macOS because the test segfaults on QWizard.show() + return + from PyQt5.QtWidgets import QWizard from nw.gui.projwizard import ( ProjWizardIntroPage, ProjWizardFolderPage, ProjWizardPopulatePage,