Merge pull request #9 from vkbo/test_coverage

Test coverage
Merging now that GUI tests work on Travis.
This commit is contained in:
Veronica K. Berglyd Olsen
2019-05-17 11:16:03 +02:00
committed by GitHub
15 changed files with 322 additions and 74 deletions
+15 -5
View File
@@ -1,20 +1,30 @@
dist: xenial # required for Python >= 3.7 os: linux
dist: xenial
services:
- xvfb
language: python language: python
cache: bundler
sudo: required sudo: required
addons: addons:
apt: apt:
packages: packages:
- libenchant-dev - libenchant-dev
- python3-pyqt5
- python3-pyqt5.qtsvg
python: python:
# - "3.5" - "3.5"
# - "3.6"
- "3.7" - "3.7"
# - "3.8-dev"
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
# - pip install pytest-faulthandler
- pip install pytest-xvfb
- pip install pytest-cov - pip install pytest-cov
- pip install pytest-qt
- pip install codecov - pip install codecov
script: script:
- python -m pytest --cov=nw -v - python -m pytest --cov=nw -m "project|core|gui" -v
after_success: after_success:
- codecov - codecov
after_failure:
- cat /sys/fs/cgroup/memory/memory.max_usage_in_bytes
+13 -9
View File
@@ -15,7 +15,7 @@ import getopt
from os import path, remove, rename from os import path, remove, rename
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from nw.main import NovelWriter from nw.gui.winmain import GuiMain
from nw.config import Config from nw.config import Config
__package__ = "novelWriter" __package__ = "novelWriter"
@@ -78,7 +78,7 @@ def main(sysArgs):
"logfile=", "logfile=",
"version", "version",
"config=", "config=",
"headless", "testmode",
] ]
helpMsg = ( helpMsg = (
@@ -111,7 +111,7 @@ def main(sysArgs):
toStd = True toStd = True
showTime = False showTime = False
confPath = None confPath = None
showGUI = True testMode = False
debugGUI = False debugGUI = False
# Parse Options # Parse Options
@@ -143,15 +143,15 @@ def main(sysArgs):
showTime = True showTime = True
elif inOpt in ("--config"): elif inOpt in ("--config"):
confPath = inArg confPath = inArg
elif inOpt in ("--headless"): elif inOpt in ("--testmode"):
showGUI = False testMode = True
elif inOpt in ("-D","--debuggui"): elif inOpt in ("-D","--debuggui"):
debugLevel = logging.DEBUG debugLevel = logging.DEBUG
debugStr = "{name:>20}:{lineno:<4d} {levelname:8} {message:}" debugStr = "{name:>20}:{lineno:<4d} {levelname:8} {message:}"
debugGUI = True debugGUI = True
# Set Config Options # Set Config Options
CONFIG.showGUI = showGUI CONFIG.showGUI = not testMode
CONFIG.debugGUI = debugGUI CONFIG.debugGUI = debugGUI
# Set Logging # Set Logging
@@ -179,8 +179,12 @@ def main(sysArgs):
CONFIG.initConfig(confPath) CONFIG.initConfig(confPath)
nwApp = QApplication([]) if testMode:
nwGUI = NovelWriter() nwGUI = GuiMain()
exit(nwApp.exec_()) return nwGUI
else:
nwApp = QApplication([])
nwGUI = GuiMain()
exit(nwApp.exec_())
return return
+15 -8
View File
@@ -117,7 +117,11 @@ class Config:
logger.debug("Loading config file") logger.debug("Loading config file")
confParser = configparser.ConfigParser() confParser = configparser.ConfigParser()
confParser.read_file(open(path.join(self.confPath,self.confFile))) try:
confParser.read_file(open(path.join(self.confPath,self.confFile)))
except Exception as e:
logger.error("Could not load config file")
return False
# Get options # Get options
@@ -182,7 +186,7 @@ class Config:
if confParser.has_option(cnfSec,"recent%d" % i): if confParser.has_option(cnfSec,"recent%d" % i):
self.recentList[i] = confParser.get(cnfSec,"recent%d" % i) self.recentList[i] = confParser.get(cnfSec,"recent%d" % i)
return return True
def saveConfig(self): def saveConfig(self):
@@ -266,13 +270,14 @@ class Config:
return return
def setConfPath(self, newPath): def setConfPath(self, newPath):
if newPath is None: return if newPath is None:
return True
if not path.isfile(newPath): if not path.isfile(newPath):
logger.error("Config: File not found. Using default config path instead.") logger.error("Config: File not found. Using default config path instead.")
return return False
self.confPath = path.dirname(newPath) self.confPath = path.dirname(newPath)
self.confFile = path.basename(newPath) self.confFile = path.basename(newPath)
return return True
def setWinSize(self, newWidth, newHeight): def setWinSize(self, newWidth, newHeight):
if abs(self.winGeometry[self.WIN_WIDTH] - newWidth) >= 10: if abs(self.winGeometry[self.WIN_WIDTH] - newWidth) >= 10:
@@ -281,14 +286,16 @@ class Config:
if abs(self.winGeometry[self.WIN_HEIGHT] - newHeight) >= 10: if abs(self.winGeometry[self.WIN_HEIGHT] - newHeight) >= 10:
self.winGeometry[self.WIN_HEIGHT] = newHeight self.winGeometry[self.WIN_HEIGHT] = newHeight
self.confChanged = True self.confChanged = True
return return True
def setTreeColWidths(self, colWidths): def setTreeColWidths(self, colWidths):
self.treeColWidth = colWidths self.treeColWidth = colWidths
return self.confChanged = True
return True
def setMainPanePos(self, panePos): def setMainPanePos(self, panePos):
self.mainPanePos = panePos self.mainPanePos = panePos
return self.confChanged = True
return True
# End Class Config # End Class Config
+11 -10
View File
@@ -282,20 +282,21 @@ class GuiDocTree(QTreeWidget):
def propagateCount(self, tHandle, theCount, nDepth=0): def propagateCount(self, tHandle, theCount, nDepth=0):
tItem = self._getTreeItem(tHandle) tItem = self._getTreeItem(tHandle)
tItem.setText(self.C_COUNT,str(theCount)) if tItem is not None:
pItem = tItem.parent() tItem.setText(self.C_COUNT,str(theCount))
if pItem is not None: pItem = tItem.parent()
pCount = 0 if pItem is not None:
for i in range(pItem.childCount()): pCount = 0
pCount += int(pItem.child(i).text(self.C_COUNT)) for i in range(pItem.childCount()):
pHandle = pItem.text(self.C_HANDLE) pCount += int(pItem.child(i).text(self.C_COUNT))
if not nDepth > 200 and pHandle != "": pHandle = pItem.text(self.C_HANDLE)
self.propagateCount(pHandle, pCount, nDepth+1) if not nDepth > 200 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
return return
def buildTree(self): def buildTree(self):
self.clear() self.clear()
for tHandle in self.theProject.projTree: for tHandle in self.theProject.treeOrder:
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
self._addTreeItem(nwItem) self._addTreeItem(nwItem)
return True return True
+6 -5
View File
@@ -170,7 +170,7 @@ class GuiMain(QMainWindow):
if self.saveProject(): if self.saveProject():
self.theProject.newProject() self.theProject.newProject()
self.treeView.buildTree() self.treeView.buildTree()
return return True
def openProject(self, projFile=None): def openProject(self, projFile=None):
if projFile is None: if projFile is None:
@@ -207,7 +207,7 @@ class GuiMain(QMainWindow):
self.stackPane.setCurrentIndex(self.stackDoc) self.stackPane.setCurrentIndex(self.stackDoc)
self.docEditor.setText(self.theDocument.openDocument(tHandle)) self.docEditor.setText(self.theDocument.openDocument(tHandle))
self.docEditor.changeWidth() self.docEditor.changeWidth()
return return True
def saveDocument(self): def saveDocument(self):
if self.theDocument.theItem is not None: if self.theDocument.theItem is not None:
@@ -217,7 +217,7 @@ class GuiMain(QMainWindow):
self.theDocument.theItem.setParaCount(self.docEditor.paraCount) self.theDocument.theItem.setParaCount(self.docEditor.paraCount)
self.theDocument.saveDocument(docHtml) self.theDocument.saveDocument(docHtml)
self.docEditor.setDocumentChanged(False) self.docEditor.setDocumentChanged(False)
return return True
def _previewDocument(self): def _previewDocument(self):
@@ -246,7 +246,7 @@ class GuiMain(QMainWindow):
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
if tHandle is None: if tHandle is None:
logger.warning("No item selected") logger.warning("No item selected")
return return False
logger.verbose("Opening item %s" % tHandle) logger.verbose("Opening item %s" % tHandle)
nwItem = self.theProject.getItem(tHandle) nwItem = self.theProject.getItem(tHandle)
@@ -255,7 +255,8 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle) self.openDocument(tHandle)
else: else:
logger.verbose("Requested item %s is not a file" % tHandle) logger.verbose("Requested item %s is not a file" % tHandle)
return
return True
def editItem(self): def editItem(self):
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
+2 -2
View File
@@ -78,6 +78,6 @@ class WordCounter(QThread):
self.paraCount += 1 self.paraCount += 1
prevEmpty = countPara == False prevEmpty = countPara == False
pass return
## END Class _WordCounter ## END Class WordCounter
-29
View File
@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Main Class
novelWriter Main Class
==========================
Sets up the main GUI and holds action and event functions
File History:
Created: 2018-09-22 [0.0.1]
"""
import logging
import nw
from nw.gui.winmain import GuiMain
logger = logging.getLogger(__name__)
class NovelWriter():
def __init__(self):
super().__init__()
self.winMain = GuiMain()
return
# END Class NovelWriter
+6
View File
@@ -0,0 +1,6 @@
[pytest]
markers =
project: Project classes tests
core: Core functionality tests
gui: Qt5 GUI tests
serial
+12 -2
View File
@@ -11,8 +11,18 @@ def ensureDir(theDir):
return return
def cmpFiles(fileOne, fileTwo, ignoreLines=[]): def cmpFiles(fileOne, fileTwo, ignoreLines=[]):
foOne = open(fileOne,mode="r")
foTwo = open(fileTwo,mode="r") try:
foOne = open(fileOne,mode="r")
except Exception as e:
print(str(e))
return False
try:
foTwo = open(fileTwo,mode="r")
except Exception as e:
print(str(e))
return False
txtOne = foOne.readlines() txtOne = foOne.readlines()
txtTwo = foTwo.readlines() txtTwo = foTwo.readlines()
+15
View File
@@ -0,0 +1,15 @@
# Hello World!
## With a Subtitle
### An Even Subier Title
#### Basically Not a Title at All
% How about a comment?
@keyword: value
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 singles quotes are replaced. We can hyphen-ate, make dashes and even longer dashes — if we want. Ellipsis? Not a problem either …
+58
View File
@@ -0,0 +1,58 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.2" fileVersion="1.0" timeStamp="2019-05-16 22:37:13">
<project>
<name></name>
<title></title>
</project>
<settings>
<spellCheck>False</spellCheck>
</settings>
<content count="6">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>0</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>0</status>
<expanded>False</expanded>
</item>
<item handle="31489056e0916" order="0" parent="25fc0e7096fc6">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>0</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
</item>
<item handle="44cb730c42048" order="1" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>0</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="2" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>0</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>0</status>
<expanded>False</expanded>
</item>
</content>
</novelWriterXML>
+5 -1
View File
@@ -2,10 +2,11 @@
"""novelWriter Common Class Tester """novelWriter Common Class Tester
""" """
import nw import nw, pytest
from nw.common import * from nw.common import *
from nwtools import cmpList from nwtools import cmpList
@pytest.mark.core
def testCheckString(): def testCheckString():
assert checkString(None, "NotNone",True) is None assert checkString(None, "NotNone",True) is None
assert checkString("None","NotNone",True) is None assert checkString("None","NotNone",True) is None
@@ -15,6 +16,7 @@ def testCheckString():
assert checkString(1.0, "NotNone",False) == "NotNone" assert checkString(1.0, "NotNone",False) == "NotNone"
assert checkString(True, "NotNone",False) == "NotNone" assert checkString(True, "NotNone",False) == "NotNone"
@pytest.mark.core
def testCheckInt(): def testCheckInt():
assert checkInt(None, 3,True) is None assert checkInt(None, 3,True) is None
assert checkInt("None",3,True) is None assert checkInt("None",3,True) is None
@@ -23,6 +25,7 @@ def testCheckInt():
assert checkInt(1.0, 3,False) == 1 assert checkInt(1.0, 3,False) == 1
assert checkInt(True, 3,False) == 1 assert checkInt(True, 3,False) == 1
@pytest.mark.core
def testCheckBool(): def testCheckBool():
assert checkBool(None, 3, True) is None assert checkBool(None, 3, True) is None
assert checkBool("None", 3, True) is None assert checkBool("None", 3, True) is None
@@ -36,6 +39,7 @@ def testCheckBool():
assert checkBool(1.0, None, False) is None assert checkBool(1.0, None, False) is None
assert checkBool(2.0, None, False) is None assert checkBool(2.0, None, False) is None
@pytest.mark.core
def testColRange(): def testColRange():
assert colRange([0,0], [0,0], 0) is None assert colRange([0,0], [0,0], 0) is None
assert cmpList(colRange([200,50,0], [50,200,0], 1), [200,50,0]) assert cmpList(colRange([200,50,0], [50,200,0], 1), [200,50,0])
+48 -1
View File
@@ -2,7 +2,7 @@
"""novelWriter Config Class Tester """novelWriter Config Class Tester
""" """
import nw import nw, pytest
from nwtools import * from nwtools import *
from os import path, unlink from os import path, unlink
from nw.config import Config from nw.config import Config
@@ -20,10 +20,57 @@ ensureDir(testTemp)
if path.isfile(tmpConf): if path.isfile(tmpConf):
unlink(tmpConf) unlink(tmpConf)
@pytest.mark.core
def testConfigInit(): def testConfigInit():
assert theConf.initConfig(testTemp) assert theConf.initConfig(testTemp)
assert cmpFiles(tmpConf, refConf, [2]) assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
@pytest.mark.core
def testConfigSave(): def testConfigSave():
assert theConf.saveConfig() assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2]) assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
@pytest.mark.core
def testConfigSetConfPath():
assert theConf.setConfPath(None)
assert not theConf.setConfPath(path.join("somewhere","over","the","rainbow"))
assert theConf.setConfPath(path.join(testTemp,"novelwriter.conf"))
assert theConf.confPath == testTemp
assert theConf.confFile == "novelwriter.conf"
assert not theConf.confChanged
@pytest.mark.core
def testConfigLoad():
assert theConf.loadConfig()
assert not theConf.confChanged
@pytest.mark.core
def testConfigSetWinSize():
assert theConf.setWinSize(1105, 655)
assert not theConf.confChanged
assert theConf.setWinSize(70,70)
assert theConf.confChanged
assert theConf.setWinSize(1100, 650)
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
@pytest.mark.core
def testConfigSetTreeColWidths():
assert theConf.setTreeColWidths([0, 0, 0])
assert theConf.confChanged
assert theConf.setTreeColWidths([120, 30, 50])
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
@pytest.mark.core
def testConfigSetMainPanePos():
assert theConf.setMainPanePos([0, 0])
assert theConf.confChanged
assert theConf.setMainPanePos([300, 800])
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""novelWriter Main GUI Class Tester
"""
import nw, pytest
from nwtools import *
from os import path, unlink
from PyQt5.QtCore import Qt
keyDelay = 10
stepDelay = 50
testDir = path.dirname(__file__)
testRef = path.join(testDir,"reference")
@pytest.mark.gui
def testMainWindows(qtbot, tmpdir):
confDir = str(tmpdir.mkdir("conf"))
projDir = str(tmpdir.mkdir("project"))
nwGUI = nw.main(["--testmode","--config=%s" % confDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
# Create new, save, open project
nwGUI.theProject.handleSeed = 42
assert nwGUI.theProject.setProjectPath(projDir)
assert nwGUI.newProject()
assert nwGUI.theProject.setProjectPath(projDir)
assert nwGUI.saveProject()
qtbot.wait(stepDelay)
assert nwGUI.openProject(projDir)
qtbot.wait(stepDelay)
# Check that tree items have been created
assert nwGUI.treeView._getTreeItem("73475cb40a568") is not None
assert nwGUI.treeView._getTreeItem("25fc0e7096fc6") is not None
assert nwGUI.treeView._getTreeItem("31489056e0916") is not None
assert nwGUI.treeView._getTreeItem("44cb730c42048") is not None
assert nwGUI.treeView._getTreeItem("71ee45a3c0db9") is not None
assert nwGUI.treeView._getTreeItem("811786ad1ae74") is not None
# Select the 'New Scene' file
nwGUI.treeView.setFocus()
nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True)
nwGUI.treeView._getTreeItem("25fc0e7096fc6").setExpanded(True)
nwGUI.treeView._getTreeItem("31489056e0916").setSelected(True)
assert nwGUI.openSelectedItem()
# Type something into the document
nwGUI.docEditor.setFocus()
for c in "# Hello World!":
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 "## With a Subtitle":
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 "### An Even Subier Title":
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 "#### Basically Not a Title at All":
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 "% How about a comment?":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
for c in "@keyword: value":
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 "This is a paragraph of dummy 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 "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)
for c in "We can also try replacing \"quotes\", even single's quotes are replaced. ":
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)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.wait(stepDelay)
nwGUI.docEditor._runCounter()
qtbot.wait(1000)
# Save the document
assert nwGUI.docEditor.docChanged
assert nwGUI.saveDocument()
qtbot.wait(stepDelay)
# Check the files
projFile = path.join(projDir,"nwProject.nwx")
assert cmpFiles(projFile, path.join(testRef,"gui_nwProject.nwx"), [2])
sceneFile = path.join(projDir,"data_3","1489056e0916_main.nwd")
assert cmpFiles(sceneFile, path.join(testRef,"gui_1489056e0916_main.nwd"))
# qtbot.stopForInteraction()
+5 -2
View File
@@ -2,8 +2,7 @@
"""novelWriter Project Class Tester """novelWriter Project Class Tester
""" """
import nw import nw, pytest, types
import types
from os import path, unlink from os import path, unlink
from nwtools import * from nwtools import *
@@ -31,20 +30,24 @@ theProject.handleSeed = 42
projFile = path.join(testProj,"nwProject.nwx") projFile = path.join(testProj,"nwProject.nwx")
@pytest.mark.project
def testProjectNew(): def testProjectNew():
assert theProject.newProject() assert theProject.newProject()
assert theProject.setProjectPath(testProj) assert theProject.setProjectPath(testProj)
assert theProject.saveProject() assert theProject.saveProject()
assert cmpFiles(projFile, path.join(testRef,"new_nwProject.nwx"), [2]) assert cmpFiles(projFile, path.join(testRef,"new_nwProject.nwx"), [2])
@pytest.mark.project
def testProjectOpen(): def testProjectOpen():
assert theProject.openProject(projFile) assert theProject.openProject(projFile)
@pytest.mark.project
def testProjectSave(): def testProjectSave():
assert theProject.saveProject() assert theProject.saveProject()
assert cmpFiles(projFile, path.join(testRef,"new_nwProject.nwx"), [2]) assert cmpFiles(projFile, path.join(testRef,"new_nwProject.nwx"), [2])
assert not theProject.projChanged assert not theProject.projChanged
@pytest.mark.project
def testProjectNewRoot(): def testProjectNewRoot():
assert theProject.openProject(projFile) assert theProject.openProject(projFile)
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None)) assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))