Merge branch 'main' into release_1.0rc2
@@ -5,20 +5,12 @@ title: ""
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Describe the Bug**
|
||||
A clear and concise description of what the bug is.
|
||||
**Note**: Issues without a description will not be considered.
|
||||
Please also check if an issue already exists on this problem.
|
||||
Please provide a description covering the following points, if applicable:
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behaviour:
|
||||
|
||||
**Expected Behaviour**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Error Message**
|
||||
If the error dialog popped up, copy/paste the content here.
|
||||
|
||||
**Additional Context**
|
||||
Add any other context about the problem here.
|
||||
* A clear and concise description of what the bug is.
|
||||
* Steps to reproduce the behaviour
|
||||
* A clear and concise description of what you expected to happen.
|
||||
* If applicable, add screenshots to help explain your problem.
|
||||
* If the error dialog popped up, copy/paste the content here.
|
||||
|
||||
@@ -5,14 +5,11 @@ title: ""
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe:**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
**Note**: Feature requests without a description will not be considered.
|
||||
Please also check if an issue already exists on this feature.
|
||||
Please provide a description covering the following points, if applicable:
|
||||
|
||||
**Describe the solution you'd like:**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered:**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context:**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
* A clear and concise description of what the problem is. Ex. I'm always frustrated when ...
|
||||
* A clear and concise description of what you want to happen.
|
||||
* A clear and concise description of any alternative solutions or features you've considered.
|
||||
* If applicable, add screenshots to help explain your request.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# novelWriter ChangeLog
|
||||
# novelWriter Change Log
|
||||
|
||||
## Version 1.0 Release Candidate 2 [2020-11-29]
|
||||
|
||||
|
||||
@@ -58,20 +58,21 @@ from nw.config import Config
|
||||
#
|
||||
|
||||
__package__ = "nw"
|
||||
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
||||
__license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "1.0rc2"
|
||||
__hexversion__ = "0x010000c2"
|
||||
__date__ = "2020-11-29"
|
||||
__date__ = "2020-12-13"
|
||||
__status__ = "Beta"
|
||||
__isstable__ = False
|
||||
__domain__ = "novelwriter.io"
|
||||
__url__ = "https://novelwriter.io"
|
||||
__sourceurl__ = "https://github.com/vkbo/novelWriter"
|
||||
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
|
||||
__domain__ = "novelwriter.io"
|
||||
__helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
|
||||
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
|
||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||
__credits__ = [
|
||||
"Veronica Berglyd Olsen (developer)",
|
||||
@@ -246,19 +247,20 @@ def main(sysArgs=None):
|
||||
errorCode |= 32
|
||||
|
||||
if errorData:
|
||||
if not testMode:
|
||||
errApp = QApplication([])
|
||||
errMsg = QErrorMessage()
|
||||
errMsg.resize(500, 300)
|
||||
errMsg.showMessage((
|
||||
"<h3>A critical error has been encountered</h3>"
|
||||
"<p>novelWriter cannot start due to the following issues:<p>"
|
||||
"<p> - %s</p>"
|
||||
"<p>Shutting down ...</p>"
|
||||
) % (
|
||||
"<br> - ".join(errorData)
|
||||
))
|
||||
errApp.exec_()
|
||||
errApp = QApplication([])
|
||||
errMsg = QErrorMessage()
|
||||
errMsg.resize(500, 300)
|
||||
errMsg.showMessage((
|
||||
"<h3>A critical error has been encountered</h3>"
|
||||
"<p>novelWriter cannot start due to the following issues:<p>"
|
||||
"<p> - %s</p>"
|
||||
"<p>Shutting down ...</p>"
|
||||
) % (
|
||||
"<br> - ".join(errorData)
|
||||
))
|
||||
for errMsg in errorData:
|
||||
logger.critical(errMsg)
|
||||
errApp.exec_()
|
||||
sys.exit(errorCode)
|
||||
|
||||
# Finish initialising config
|
||||
@@ -293,4 +295,4 @@ def main(sysArgs=None):
|
||||
nwGUI = GuiMain()
|
||||
sys.exit(nwApp.exec_())
|
||||
|
||||
return
|
||||
# END Function main
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,207 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 270.93333 270.93334"
|
||||
height="1024"
|
||||
width="1024">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
id="linearGradient2515">
|
||||
<stop
|
||||
style="stop-color:#f7f7f7;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2511" />
|
||||
<stop
|
||||
style="stop-color:#e5e5e5;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2513" />
|
||||
</linearGradient>
|
||||
<rect
|
||||
x="60.484347"
|
||||
y="205.79962"
|
||||
width="130.57787"
|
||||
height="65.785888"
|
||||
id="rect852" />
|
||||
<rect
|
||||
id="rect1474"
|
||||
height="28.631245"
|
||||
width="177.70083"
|
||||
y="18.019813"
|
||||
x="13.463703" />
|
||||
<rect
|
||||
id="rect852-7"
|
||||
height="65.785889"
|
||||
width="130.57787"
|
||||
y="205.79962"
|
||||
x="60.484348" />
|
||||
<rect
|
||||
id="rect1544"
|
||||
height="65.785889"
|
||||
width="130.57787"
|
||||
y="205.79962"
|
||||
x="60.484348" />
|
||||
<rect
|
||||
x="60.484348"
|
||||
y="205.79962"
|
||||
width="130.57787"
|
||||
height="65.785889"
|
||||
id="rect1570" />
|
||||
<rect
|
||||
x="13.463703"
|
||||
y="18.019813"
|
||||
width="177.70083"
|
||||
height="28.631245"
|
||||
id="rect1587" />
|
||||
<filter
|
||||
id="filter1444"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
id="feFlood1434"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.498039" />
|
||||
<feComposite
|
||||
id="feComposite1436"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1438"
|
||||
result="blur"
|
||||
stdDeviation="3"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1440"
|
||||
result="offset"
|
||||
dy="6"
|
||||
dx="6" />
|
||||
<feComposite
|
||||
id="feComposite1442"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1456"
|
||||
style="color-interpolation-filters:sRGB">
|
||||
<feFlood
|
||||
id="feFlood1446"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.498039" />
|
||||
<feComposite
|
||||
id="feComposite1448"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1450"
|
||||
result="blur"
|
||||
stdDeviation="3"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1452"
|
||||
result="offset"
|
||||
dy="6"
|
||||
dx="6" />
|
||||
<feComposite
|
||||
id="feComposite1454"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
</filter>
|
||||
<radialGradient
|
||||
xlink:href="#linearGradient2515"
|
||||
id="radialGradient2517"
|
||||
cx="605.42725"
|
||||
cy="623.97186"
|
||||
fx="605.42725"
|
||||
fy="623.97186"
|
||||
r="410"
|
||||
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer3"
|
||||
style="display:inline">
|
||||
<path
|
||||
clip-path="none"
|
||||
transform="scale(0.26458333)"
|
||||
d="m 192,22 c -44.32,0 -80,35.680002 -80,80 v 820 c 0,44.32002 35.68,80 80,80 h 640 c 44.31999,0 80,-35.67998 80,-80 V 213.44727 L 720.55469,22 Z"
|
||||
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1481" />
|
||||
<path
|
||||
id="path1490"
|
||||
style="display:inline;fill:#e0e0e0;fill-opacity:1;stroke:#999999;stroke-width:5.29167;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 241.30037,56.474215 190.64713,5.8204593 V 35.307549 c 0,11.726333 9.44034,21.166666 21.16667,21.166666 z" />
|
||||
<g
|
||||
aria-label=".nwx"
|
||||
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
|
||||
id="text850"
|
||||
style="font-size:5.64444px;line-height:1.25;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans, Normal';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect852);display:inline;fill:#666666;fill-opacity:1">
|
||||
<path
|
||||
d="m 63.088865,241.76928 q 0,-1.36426 0.909505,-2.27376 0.930175,-0.93018 2.480467,-0.93018 1.550291,0 2.459796,0.93018 0.909504,0.9095 0.909504,2.27376 0,1.34359 -0.909504,2.27376 -0.909505,0.9095 -2.459796,0.9095 -1.550292,0 -2.480467,-0.9095 -0.909505,-0.93017 -0.909505,-2.27376 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||
id="path856" />
|
||||
<path
|
||||
d="m 84.75161,226.78312 q -2.542479,0 -3.782712,2.14974 v 15.79231 h -5.973791 v -22.36554 h 5.601721 l 0.206705,2.56314 q 2.397785,-2.97656 6.428544,-2.97656 3.162595,0 5.105627,1.88103 1.943033,1.88102 1.963703,6.55256 v 14.34537 h -5.994461 v -14.26269 q 0,-2.14973 -0.930175,-2.91454 -0.909505,-0.76482 -2.625161,-0.76482 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||
id="path858" />
|
||||
<path
|
||||
d="m 126.96089,222.35963 -5.68441,22.36554 h -5.00227 l -4.25814,-14.07665 -4.23746,14.07665 h -4.9816 l -5.705078,-22.36554 h 5.767088 l 2.95589,14.22134 4.05143,-14.22134 h 4.32014 l 4.09277,14.20067 2.91455,-14.20067 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||
id="path860" />
|
||||
<path
|
||||
d="m 134.77436,222.35963 3.65869,6.65592 3.76204,-6.65592 h 6.3872 l -6.36653,10.93472 6.63525,11.43082 h -6.3872 l -4.01009,-7.04866 -3.94808,7.04866 h -6.42854 l 6.63525,-11.43082 -6.36654,-10.93472 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
|
||||
id="path862" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g2509"
|
||||
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)">
|
||||
<path
|
||||
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
|
||||
id="rect1406-1"
|
||||
style="display:inline;fill:#a11212;fill-opacity:1;stroke:#7e1b1b;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1444)"
|
||||
d="m 145.18984,56.414257 c -44.32,0 -79.999998,35.68 -79.999998,80.000003 v 199.99805 a 279.99999,279.99999 0 0 0 0,0.002 279.99999,279.99999 0 0 0 0,0.0195 v 199.98242 c 0,44.31999 35.679998,80 79.999998,80 44.32,0 80,-35.68001 80,-80 v -200 a 120,120 0 0 1 120,-120 120,120 0 0 1 120,120 v 282.83592 h 160 V 336.41621 a 279.99999,279.99999 0 0 0 -280,-280.000003 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 c -14.4245,-20.75262 -38.42829,-34.29883 -65.72461,-34.29883 z M 376.65078,619.64082 c -0.62174,0.0972 -1.2172,0.22317 -1.7793,0.37696 0.5621,-0.15379 1.15756,-0.27973 1.7793,-0.37696 z m -1.7793,0.37696 c -0.5621,0.15378 -1.09071,0.33534 -1.58008,0.53906 0.48937,-0.20372 1.01798,-0.38528 1.58008,-0.53906 z m 338.85742,-0.37696 c 0.62174,0.0972 1.2172,0.22317 1.7793,0.37696 -0.5621,-0.15379 -1.15756,-0.27973 -1.7793,-0.37696 z m 1.7793,0.37696 c 0.5621,0.15378 1.09071,0.33534 1.58008,0.53906 -0.48938,-0.20372 -1.01798,-0.38528 -1.58008,-0.53906 z" />
|
||||
<path
|
||||
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
|
||||
id="path897"
|
||||
style="display:inline;fill:#730d0d;fill-opacity:1;stroke:#590d0d;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1456)"
|
||||
d="m 465.18984,619.25215 v 0.16406 h -85.58398 c -5.49302,0 -9.91602,2.2329 -9.91602,5.00586 0,0.52294 0.15913,1.02696 0.45117,1.5 h -0.44531 c 0.30916,2.03607 1.24102,3.99791 2.81641,5.57227 l 165.59961,165.49414 c 3.92061,3.91808 10.23371,3.91812 14.15429,0 L 717.86757,631.49434 c 1.57569,-1.57463 2.50725,-3.53578 2.81641,-5.57227 h -0.44531 c 0.29216,-0.47304 0.45117,-0.97706 0.45117,-1.5 0,-2.77296 -4.42304,-5.00586 -9.91602,-5.00586 h -85.58398 v -0.16406 z" />
|
||||
<rect
|
||||
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="rect914"
|
||||
width="6.7583966"
|
||||
height="82.482399"
|
||||
x="-118.27303"
|
||||
y="1.2969339"
|
||||
rx="3.3791983"
|
||||
ry="3.6658845" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<body>
|
||||
<h2>Release Notes for 1.0 RC1</h2>
|
||||
<p>This is the first release candidate for the upcoming release of novelWriter 1.0.</p>
|
||||
<p>Since the fifth beta release about four weeks ago, not much has been changed in novelWriter. A
|
||||
few minor tweaks have been made to the GUI.</p>
|
||||
<p>A number of features and tools are now automatically switched off when there is no project or
|
||||
document open for those features to act upon. Previously, this was a bit inconsistent, although no
|
||||
serious bugs have been reported or encountered.</p>
|
||||
<p>Most of the minor changes in this release should not be noticeable to most users. However, there
|
||||
are a couple of noticeable changes.</p>
|
||||
<h3>Typewriter Mode</h3>
|
||||
<p>The "Typewriter Mode" of the editor has been improved. Essentially, this feature is a sort of
|
||||
smart scroll. It tries to keep the cursor stationary in the vertical direction, and will try to
|
||||
scroll the document up when the cursor skips to a new line while typing (or down in case of
|
||||
backspaces). This is similar to the way a typewriter scrolls the paper when hitting the return key.
|
||||
It improves the writing experience as the current active line will stay at the same eye height
|
||||
level on the screen.</p>
|
||||
<p>Previously, the feature would lock the cursor to a given vertical position defined by the user.
|
||||
Now, instead, the cursor will remain stationary in the vertical direction at any position the user
|
||||
sets it to by mouse click or keyboard navigation. The user can define a minimum distance from the
|
||||
top where this feature is activated. These changes makes it more flexible in terms of where the
|
||||
focus is in the editor. The feature can be controlled from the main Preferences.</p>
|
||||
<h3>Switching Syntax Theme</h3>
|
||||
<p>It is now possible to switch syntax highlighting theme without restarting novelWriter.
|
||||
Previously, changing the theme would only half-way update the document, header and footer
|
||||
background and text colours. The new settings would not be fully applied until the application was
|
||||
shut down and started again, thus making it a bit tedious to look through syntax themes to find the
|
||||
one you want.</p>
|
||||
<p>Switching main GUI theme still requires a restart.</p>
|
||||
<p><i>The full changelog is available
|
||||
<a href="https://github.com/vkbo/novelWriter/blob/main/CHANGELOG.md">here</a>.</i></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,7 +10,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
background = 255, 255, 255
|
||||
text = 0, 0, 0
|
||||
link = 0, 0, 200
|
||||
headertext = 0, 100, 00
|
||||
headertext = 0, 100, 0
|
||||
headertag = 50, 100, 50
|
||||
emphasis = 150, 110, 30
|
||||
straightquotes = 200, 0, 0
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[Main]
|
||||
name = Grey Dark
|
||||
author = Veronica Berglyd Olsen
|
||||
credit = Veronica Berglyd Olsen
|
||||
url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
[Syntax]
|
||||
background = 54, 54, 54
|
||||
text = 200, 200, 200
|
||||
link = 200, 200, 200
|
||||
headertext = 225, 225, 225
|
||||
headertag = 225, 225, 225
|
||||
emphasis = 200, 200, 200
|
||||
straightquotes = 200, 200, 200
|
||||
doublequotes = 200, 200, 200
|
||||
singlequotes = 200, 200, 200
|
||||
hidden = 150, 150, 150
|
||||
keyword = 225, 225, 225
|
||||
value = 200, 200, 200
|
||||
spellcheckline = 200, 46, 0
|
||||
tagerror = 46, 200, 0
|
||||
replacetag = 225, 225, 225
|
||||
modifier = 225, 225, 225
|
||||
@@ -0,0 +1,25 @@
|
||||
[Main]
|
||||
name = Grey Light
|
||||
author = Veronica Berglyd Olsen
|
||||
credit = Veronica Berglyd Olsen
|
||||
url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
[Syntax]
|
||||
background = 255, 255, 255
|
||||
text = 20, 20, 20
|
||||
link = 20, 20, 20
|
||||
headertext = 0, 0, 0
|
||||
headertag = 0, 0, 0
|
||||
emphasis = 20, 20, 20
|
||||
straightquotes = 20, 20, 20
|
||||
doublequotes = 20, 20, 20
|
||||
singlequotes = 20, 20, 20
|
||||
hidden = 100, 100, 100
|
||||
keyword = 0, 0, 0
|
||||
value = 20, 20, 20
|
||||
spellcheckline = 200, 0, 0
|
||||
tagerror = 0, 150, 0
|
||||
replacetag = 0, 0, 0
|
||||
modifier = 0, 0, 0
|
||||
@@ -27,12 +27,12 @@
|
||||
|
||||
import logging
|
||||
import configparser
|
||||
import shutil
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
from time import time
|
||||
from shutil import which
|
||||
|
||||
from PyQt5.Qt import PYQT_VERSION_STR
|
||||
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
|
||||
@@ -44,10 +44,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Config:
|
||||
|
||||
CNF_STR = 0
|
||||
CNF_INT = 1
|
||||
CNF_BOOL = 2
|
||||
CNF_LIST = 3
|
||||
CNF_STR = 0
|
||||
CNF_INT = 1
|
||||
CNF_BOOL = 2
|
||||
CNF_S_LST = 3
|
||||
CNF_I_LST = 4
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -91,6 +92,7 @@ class Config:
|
||||
self.guiFont = "" # Defaults to system default font
|
||||
self.guiFontSize = 11
|
||||
self.guiScale = 1.0 # Set automatically by Theme class
|
||||
self.lastNotes = "" # The latest release notes that have been shown
|
||||
|
||||
## Sizes
|
||||
self.winGeometry = [1200, 650]
|
||||
@@ -210,12 +212,8 @@ class Config:
|
||||
self.osUnknown = True
|
||||
|
||||
# Other System Info
|
||||
if self.verQtValue >= 50600:
|
||||
self.hostName = QSysInfo.machineHostName()
|
||||
self.kernelVer = QSysInfo.kernelVersion()
|
||||
else:
|
||||
self.hostName = "Unknown"
|
||||
self.kernelVer = "Unknown"
|
||||
self.hostName = "Unknown"
|
||||
self.kernelVer = "Unknown"
|
||||
|
||||
# Packages
|
||||
self.hasEnchant = False # The pyenchant package
|
||||
@@ -320,6 +318,11 @@ class Config:
|
||||
self.errData.append(str(e))
|
||||
self.dataPath = None
|
||||
|
||||
# Host and Kernel
|
||||
if self.verQtValue >= 50600:
|
||||
self.hostName = QSysInfo.machineHostName()
|
||||
self.kernelVer = QSysInfo.kernelVersion()
|
||||
|
||||
# Load recent projects cache
|
||||
self.loadRecentCache()
|
||||
|
||||
@@ -380,29 +383,32 @@ class Config:
|
||||
self.guiFontSize = self._parseLine(
|
||||
cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize
|
||||
)
|
||||
self.lastNotes = self._parseLine(
|
||||
cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes
|
||||
)
|
||||
|
||||
## Sizes
|
||||
cnfSec = "Sizes"
|
||||
self.winGeometry = self._parseLine(
|
||||
cnfParse, cnfSec, "geometry", self.CNF_LIST, self.winGeometry
|
||||
cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry
|
||||
)
|
||||
self.treeColWidth = self._parseLine(
|
||||
cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth
|
||||
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
|
||||
)
|
||||
self.projColWidth = self._parseLine(
|
||||
cnfParse, cnfSec, "projcols", self.CNF_LIST, self.projColWidth
|
||||
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
|
||||
)
|
||||
self.mainPanePos = self._parseLine(
|
||||
cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos
|
||||
cnfParse, cnfSec, "mainpane", self.CNF_I_LST, self.mainPanePos
|
||||
)
|
||||
self.docPanePos = self._parseLine(
|
||||
cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos
|
||||
cnfParse, cnfSec, "docpane", self.CNF_I_LST, self.docPanePos
|
||||
)
|
||||
self.viewPanePos = self._parseLine(
|
||||
cnfParse, cnfSec, "viewpane", self.CNF_LIST, self.viewPanePos
|
||||
cnfParse, cnfSec, "viewpane", self.CNF_I_LST, self.viewPanePos
|
||||
)
|
||||
self.outlnPanePos = self._parseLine(
|
||||
cnfParse, cnfSec, "outlinepane", self.CNF_LIST, self.outlnPanePos
|
||||
cnfParse, cnfSec, "outlinepane", self.CNF_I_LST, self.outlnPanePos
|
||||
)
|
||||
self.isFullScreen = self._parseLine(
|
||||
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
|
||||
@@ -480,10 +486,10 @@ class Config:
|
||||
cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos
|
||||
)
|
||||
self.fmtSingleQuotes = self._parseLine(
|
||||
cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes
|
||||
cnfParse, cnfSec, "fmtsinglequote", self.CNF_S_LST, self.fmtSingleQuotes
|
||||
)
|
||||
self.fmtDoubleQuotes = self._parseLine(
|
||||
cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes
|
||||
cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes
|
||||
)
|
||||
self.spellTool = self._parseLine(
|
||||
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
|
||||
@@ -584,6 +590,7 @@ class Config:
|
||||
cnfParse.set(cnfSec, "guidark", str(self.guiDark))
|
||||
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
|
||||
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
|
||||
cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes))
|
||||
|
||||
## Sizes
|
||||
cnfSec = "Sizes"
|
||||
@@ -896,23 +903,28 @@ class Config:
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _unpackList(self, inStr, listLen, listDefault, castTo=int):
|
||||
"""Unpack a comma separated string of items into a list.
|
||||
"""
|
||||
inData = inStr.split(",")
|
||||
outData = []
|
||||
for i in range(listLen):
|
||||
try:
|
||||
outData.append(castTo(inData[i]))
|
||||
except Exception:
|
||||
outData.append(listDefault[i])
|
||||
return outData
|
||||
|
||||
def _packList(self, inData):
|
||||
"""Pack a list of items into a comma separated string.
|
||||
"""Pack a list of items into a comma-separated string.
|
||||
"""
|
||||
return ", ".join([str(inVal) for inVal in inData])
|
||||
|
||||
def _unpackList(self, inStr, listDefault, cnfType):
|
||||
"""Unpack a comma-separated string of items into a list.
|
||||
"""
|
||||
inData = inStr.split(",")
|
||||
outData = listDefault.copy()
|
||||
for i in range(min(len(inData), len(listDefault))):
|
||||
try:
|
||||
if cnfType == self.CNF_S_LST:
|
||||
outData[i] = inData[i].strip()
|
||||
elif cnfType == self.CNF_I_LST:
|
||||
outData[i] = int(inData[i].strip())
|
||||
else:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
return outData
|
||||
|
||||
def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault):
|
||||
"""Parse a line and return the correct datatype.
|
||||
"""
|
||||
@@ -925,18 +937,24 @@ class Config:
|
||||
return cnfParse.getint(cnfSec, cnfName)
|
||||
elif cnfType == self.CNF_BOOL:
|
||||
return cnfParse.getboolean(cnfSec, cnfName)
|
||||
elif cnfType == self.CNF_LIST:
|
||||
elif cnfType == self.CNF_I_LST:
|
||||
return self._unpackList(
|
||||
cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault
|
||||
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_I_LST
|
||||
)
|
||||
elif cnfType == self.CNF_S_LST:
|
||||
return self._unpackList(
|
||||
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to load value from config file.")
|
||||
logger.error(str(e))
|
||||
return cnfDefault
|
||||
|
||||
return cnfDefault
|
||||
|
||||
def _checkNone(self, checkVal):
|
||||
"""Convert a string to a none type.
|
||||
"""Return a NoneType if the value correspomds to None, otherwise
|
||||
return the value unchanged.
|
||||
"""
|
||||
if checkVal is None:
|
||||
return None
|
||||
@@ -956,10 +974,8 @@ class Config:
|
||||
self.hasEnchant = False
|
||||
logger.debug("Checking package 'pyenchant': Missing")
|
||||
|
||||
try:
|
||||
self.hasAssistant = which("assistant")
|
||||
except Exception:
|
||||
self.hasAssistant = False
|
||||
assistPath = shutil.which("assistant")
|
||||
self.hasAssistant = assistPath is not None
|
||||
if self.hasAssistant:
|
||||
logger.debug("Checking executable 'assistant': Ok")
|
||||
else:
|
||||
|
||||
@@ -168,7 +168,8 @@ class NWDoc():
|
||||
os.unlink(docPath)
|
||||
os.rename(docTemp, docPath)
|
||||
|
||||
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||
if self._theItem is not None:
|
||||
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class NWItem():
|
||||
self.itemName = ""
|
||||
self.itemHandle = None
|
||||
self.itemParent = None
|
||||
self.itemOrder = None
|
||||
self.itemOrder = 0
|
||||
self.itemType = nwItemType.NO_TYPE
|
||||
self.itemClass = nwItemClass.NO_CLASS
|
||||
self.itemLayout = nwItemLayout.NO_LAYOUT
|
||||
@@ -96,20 +96,21 @@ class NWItem():
|
||||
return False
|
||||
|
||||
if "handle" in xItem.attrib:
|
||||
self.itemHandle = xItem.attrib["handle"]
|
||||
self.setHandle(xItem.attrib["handle"])
|
||||
else:
|
||||
logger.error("XML item entry does not have a handle")
|
||||
return False
|
||||
|
||||
if "parent" in xItem.attrib:
|
||||
self.itemParent = xItem.attrib["parent"]
|
||||
self.setParent(xItem.attrib["parent"])
|
||||
|
||||
if "order" in xItem.attrib:
|
||||
self.setOrder(xItem.attrib["order"])
|
||||
|
||||
retStatus = True
|
||||
for xValue in xItem:
|
||||
if xValue.tag == "name":
|
||||
self.setName(xValue.text)
|
||||
elif xValue.tag == "order":
|
||||
self.setOrder(xValue.text)
|
||||
elif xValue.tag == "type":
|
||||
self.setType(xValue.text)
|
||||
elif xValue.tag == "class":
|
||||
@@ -156,7 +157,10 @@ class NWItem():
|
||||
def setName(self, theName):
|
||||
"""Set the item name.
|
||||
"""
|
||||
self.itemName = theName.strip()
|
||||
if isinstance(theName, str):
|
||||
self.itemName = theName.strip()
|
||||
else:
|
||||
self.itemName = ""
|
||||
return
|
||||
|
||||
def setHandle(self, theHandle):
|
||||
|
||||
@@ -176,11 +176,7 @@ class OptionState():
|
||||
"""
|
||||
if getGroup in self.theState:
|
||||
if getName in self.theState[getGroup]:
|
||||
try:
|
||||
return self.theState[getGroup][getName]
|
||||
except Exception as e:
|
||||
logger.warning(str(e))
|
||||
return defaultValue
|
||||
return self.theState[getGroup][getName]
|
||||
return defaultValue
|
||||
|
||||
def getString(self, getGroup, getName, defaultValue):
|
||||
@@ -189,11 +185,7 @@ class OptionState():
|
||||
"""
|
||||
if getGroup in self.theState:
|
||||
if getName in self.theState[getGroup]:
|
||||
try:
|
||||
return str(self.theState[getGroup][getName])
|
||||
except Exception as e:
|
||||
logger.warning(str(e))
|
||||
return defaultValue
|
||||
return str(self.theState[getGroup][getName])
|
||||
return defaultValue
|
||||
|
||||
def getInt(self, getGroup, getName, defaultValue):
|
||||
@@ -228,11 +220,7 @@ class OptionState():
|
||||
"""
|
||||
if getGroup in self.theState:
|
||||
if getName in self.theState[getGroup]:
|
||||
try:
|
||||
return bool(self.theState[getGroup][getName])
|
||||
except Exception as e:
|
||||
logger.warning(str(e))
|
||||
return defaultValue
|
||||
return bool(self.theState[getGroup][getName])
|
||||
return defaultValue
|
||||
|
||||
##
|
||||
|
||||
@@ -28,12 +28,10 @@
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from lxml import etree
|
||||
from time import time
|
||||
from shutil import make_archive, unpack_archive, copyfile
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from nw.core.tree import NWTree
|
||||
from nw.core.item import NWItem
|
||||
@@ -436,26 +434,15 @@ class NWProject():
|
||||
xRoot = nwXML.getroot()
|
||||
nwxRoot = xRoot.tag
|
||||
|
||||
appVersion = "Unknown"
|
||||
hexVersion = "0x0"
|
||||
fileVersion = "Unknown"
|
||||
self.saveCount = 0
|
||||
self.autoCount = 0
|
||||
|
||||
if "appVersion" in xRoot.attrib:
|
||||
appVersion = xRoot.attrib["appVersion"]
|
||||
if "hexVersion" in xRoot.attrib:
|
||||
hexVersion = xRoot.attrib["hexVersion"]
|
||||
if "fileVersion" in xRoot.attrib:
|
||||
fileVersion = xRoot.attrib["fileVersion"]
|
||||
appVersion = xRoot.attrib.get("appVersion", "Unknown")
|
||||
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
|
||||
fileVersion = xRoot.attrib.get("fileVersion", "Unknown")
|
||||
|
||||
# The following are deprecated and will be removed
|
||||
if "saveCount" in xRoot.attrib:
|
||||
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
|
||||
if "autoCount" in xRoot.attrib:
|
||||
self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False)
|
||||
if "editTime" in xRoot.attrib:
|
||||
self.editTime = checkInt(xRoot.attrib["editTime"], 0, False)
|
||||
# The settings have been moved to the <project> tag
|
||||
self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False)
|
||||
self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False)
|
||||
self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False)
|
||||
|
||||
logger.verbose("XML root is %s" % nwxRoot)
|
||||
logger.verbose("File version is %s" % fileVersion)
|
||||
@@ -483,20 +470,19 @@ 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.showGUI:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(self.theParent, "Old Project Version", (
|
||||
if fileVersion == "1.0":
|
||||
msgRes = self.theParent.askQuestion("Old Project Version", (
|
||||
"The project file and data is created by a novelWriter version "
|
||||
"lower than 0.7. Do you want to upgrade the project to the "
|
||||
"most recent format?<br><br>Note that after the upgrade, you "
|
||||
"cannot open the project with an older version of novelWriter "
|
||||
"any more, so make sure you have a recent backup."
|
||||
))
|
||||
if msgRes != QMessageBox.Yes:
|
||||
if not msgRes:
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI:
|
||||
elif fileVersion != "1.1" and fileVersion != "1.2":
|
||||
self.makeAlert((
|
||||
"Unknown or unsupported novelWriter project file format. "
|
||||
"The project cannot be opened by this version of novelWriter. "
|
||||
@@ -510,9 +496,8 @@ class NWProject():
|
||||
# Check novelWriter Version
|
||||
# =========================
|
||||
|
||||
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(self.theParent, "Version Conflict", (
|
||||
if int(hexVersion, 16) > int(nw.__hexversion__, 16):
|
||||
msgRes = self.theParent.askQuestion("Version Conflict", (
|
||||
"This project was saved by a newer version of novelWriter, version %s. "
|
||||
"This is version %s. If you continue to open the project, some attributes "
|
||||
"and settings may not be preserved, but the overall project should be fine. "
|
||||
@@ -520,7 +505,7 @@ class NWProject():
|
||||
) % (
|
||||
appVersion, nw.__version__
|
||||
))
|
||||
if msgRes != QMessageBox.Yes:
|
||||
if not msgRes:
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
@@ -835,15 +820,15 @@ class NWProject():
|
||||
|
||||
try:
|
||||
self._clearLockFile()
|
||||
make_archive(baseName, "zip", self.projPath, ".")
|
||||
shutil.make_archive(baseName, "zip", self.projPath, ".")
|
||||
self._writeLockFile()
|
||||
logger.info("Backup written to: %s" % archName)
|
||||
if doNotify:
|
||||
self.theParent.makeAlert(
|
||||
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
|
||||
nwAlert.INFO
|
||||
)
|
||||
else:
|
||||
logger.info("Backup written to: %s" % archName)
|
||||
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not write backup archive.", str(e)],
|
||||
@@ -874,7 +859,7 @@ class NWProject():
|
||||
|
||||
self.setProjectPath(projPath, newProject=True)
|
||||
try:
|
||||
unpack_archive(pkgSample, projPath)
|
||||
shutil.unpack_archive(pkgSample, projPath)
|
||||
isSuccess = True
|
||||
except Exception as e:
|
||||
self.makeAlert(
|
||||
@@ -887,14 +872,14 @@ class NWProject():
|
||||
try:
|
||||
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
|
||||
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
|
||||
copyfile(srcProj, dstProj)
|
||||
shutil.copyfile(srcProj, dstProj)
|
||||
|
||||
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)
|
||||
shutil.copyfile(srcDoc, dstDoc)
|
||||
|
||||
isSuccess = True
|
||||
|
||||
@@ -998,11 +983,15 @@ class NWProject():
|
||||
"You must set a valid backup path in preferences to use "
|
||||
"the automatic project backup feature."
|
||||
), nwAlert.WARN)
|
||||
return False
|
||||
|
||||
if self.projName == "":
|
||||
self.theParent.makeAlert((
|
||||
"You must set a valid project name in project settings to "
|
||||
"use the automatic project backup feature."
|
||||
), nwAlert.WARN)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def setSpellCheck(self, theMode):
|
||||
@@ -1011,12 +1000,15 @@ class NWProject():
|
||||
if self.spellCheck != theMode:
|
||||
self.spellCheck = theMode
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
return self.spellCheck
|
||||
|
||||
def setSpellLang(self, theLang):
|
||||
"""Set the project-specific spell check language.
|
||||
"""
|
||||
self.projLang = checkString(theLang, None, True)
|
||||
theLang = checkString(theLang, None, True)
|
||||
if self.projLang != theLang:
|
||||
self.projLang = theLang
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
|
||||
def setAutoOutline(self, theMode):
|
||||
@@ -1025,7 +1017,7 @@ class NWProject():
|
||||
if self.autoOutline != theMode:
|
||||
self.autoOutline = theMode
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
return self.autoOutline
|
||||
|
||||
def setTreeOrder(self, newOrder):
|
||||
"""A list representing the linear/flattened order of project
|
||||
@@ -1072,7 +1064,7 @@ class NWProject():
|
||||
if nwItem.itemStatus in replaceMap.keys():
|
||||
nwItem.setStatus(replaceMap[nwItem.itemStatus])
|
||||
self.setProjectChanged(True)
|
||||
return
|
||||
return True
|
||||
|
||||
def setImportColours(self, newCols):
|
||||
"""Update the list of note file importance flags. Also iterate
|
||||
@@ -1084,14 +1076,15 @@ class NWProject():
|
||||
if nwItem.itemStatus in replaceMap.keys():
|
||||
nwItem.setStatus(replaceMap[nwItem.itemStatus])
|
||||
self.setProjectChanged(True)
|
||||
return
|
||||
return True
|
||||
|
||||
def setAutoReplace(self, autoReplace):
|
||||
"""Update the auto-replace dictionary. This replaces the entire
|
||||
dictionary, so alterations have to be made in a copy.
|
||||
"""
|
||||
self.autoReplace = autoReplace
|
||||
return
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
|
||||
def setTitleFormat(self, titleFormat):
|
||||
"""Set the formatting of titles in the project.
|
||||
@@ -1099,7 +1092,7 @@ class NWProject():
|
||||
for valKey, valEntry in titleFormat.items():
|
||||
if valKey in self.titleFormat:
|
||||
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
|
||||
return
|
||||
return True
|
||||
|
||||
def setProjectChanged(self, bValue):
|
||||
"""Toggle the project changed flag, and propagate the
|
||||
@@ -1132,13 +1125,11 @@ class NWProject():
|
||||
sentItems = []
|
||||
iterItems = self.projTree.handles()
|
||||
n = 0
|
||||
nMax = len(iterItems)
|
||||
nMax = min(len(iterItems), 10000)
|
||||
while n < nMax:
|
||||
tHandle = iterItems[n]
|
||||
tItem = self.projTree[tHandle]
|
||||
n += 1
|
||||
if n > 10000:
|
||||
return # Just in case
|
||||
if tItem is None:
|
||||
# Technically a bug since treeOrder is built from the
|
||||
# same data as projTree
|
||||
@@ -1154,10 +1145,11 @@ class NWProject():
|
||||
yield tItem
|
||||
elif tItem.itemParent in iterItems:
|
||||
# Item's parent exists, but hasn't been sent yet, so add
|
||||
# it again to the end
|
||||
# it again to the end, but make sure this doesn't get
|
||||
# out hand, so we cap at 10000 items
|
||||
logger.warning("Item %s found before its parent" % tHandle)
|
||||
iterItems.append(tHandle)
|
||||
nMax = len(iterItems)
|
||||
nMax = min(len(iterItems), 10000)
|
||||
else:
|
||||
# Item is orphaned
|
||||
logger.error("Item %s has no parent in current tree" % tHandle)
|
||||
@@ -1196,13 +1188,12 @@ class NWProject():
|
||||
if not os.path.isfile(lockFile):
|
||||
return []
|
||||
|
||||
theLines = []
|
||||
try:
|
||||
with open(lockFile, mode="r", encoding="utf8") as inFile:
|
||||
theData = inFile.read()
|
||||
theLines = theData.splitlines()
|
||||
if len(theLines) == 4:
|
||||
return theLines
|
||||
else:
|
||||
if len(theLines) != 4:
|
||||
return ["ERROR"]
|
||||
|
||||
except Exception as e:
|
||||
@@ -1210,7 +1201,7 @@ class NWProject():
|
||||
logger.error(str(e))
|
||||
return ["ERROR"]
|
||||
|
||||
return ["ERROR"]
|
||||
return theLines
|
||||
|
||||
def _writeLockFile(self):
|
||||
"""Writes a lock file to the project folder.
|
||||
@@ -1243,13 +1234,12 @@ class NWProject():
|
||||
if os.path.isfile(lockFile):
|
||||
try:
|
||||
os.unlink(lockFile)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Failed to remove project lockfile")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
|
||||
return None
|
||||
return True
|
||||
|
||||
def _checkFolder(self, thePath):
|
||||
"""Check if a folder exists, and if it doesn't, create it.
|
||||
@@ -1292,7 +1282,7 @@ class NWProject():
|
||||
back into the project tree.
|
||||
"""
|
||||
if self.projPath is None:
|
||||
return
|
||||
return False
|
||||
|
||||
# Then check the files in the data folder
|
||||
logger.debug("Checking files in project content folder")
|
||||
@@ -1352,7 +1342,7 @@ class NWProject():
|
||||
orphItem.setLayout(oLayout)
|
||||
self.projTree.append(oHandle, None, orphItem)
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def _appendSessionStats(self):
|
||||
"""Append session statistics to the sessions log file.
|
||||
@@ -1363,21 +1353,27 @@ class NWProject():
|
||||
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:
|
||||
# It's a new file, so add a header
|
||||
if self.lastWCount > 0:
|
||||
outFile.write("# Offset %d\n" % self.lastWCount)
|
||||
outFile.write("# %-17s %-19s %8s %8s\n" % (
|
||||
"Start Time", "End Time", "Novel", "Notes"
|
||||
try:
|
||||
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
|
||||
if not isFile:
|
||||
# It's a new file, so add a header
|
||||
if self.lastWCount > 0:
|
||||
outFile.write("# Offset %d\n" % self.lastWCount)
|
||||
outFile.write("# %-17s %-19s %8s %8s\n" % (
|
||||
"Start Time", "End Time", "Novel", "Notes"
|
||||
))
|
||||
|
||||
outFile.write("%-19s %-19s %8d %8d\n" % (
|
||||
formatTimeStamp(self.projOpened),
|
||||
formatTimeStamp(time()),
|
||||
self.novelWCount,
|
||||
self.notesWCount,
|
||||
))
|
||||
|
||||
outFile.write("%-19s %-19s %8d %8d\n" % (
|
||||
formatTimeStamp(self.projOpened),
|
||||
formatTimeStamp(time()),
|
||||
self.novelWCount,
|
||||
self.notesWCount,
|
||||
))
|
||||
except Exception as e:
|
||||
logger.error("Failed to write session stats file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1490,7 +1486,8 @@ class NWProject():
|
||||
os.unlink(rmFile)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
return False
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
# END Class NWProject
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
|
||||
from difflib import get_close_matches
|
||||
import difflib
|
||||
|
||||
from nw.constants import nwConst, isoLanguage
|
||||
|
||||
@@ -70,14 +69,16 @@ class NWSpellCheck():
|
||||
"""
|
||||
if self.projectDict is not None and newWord not in self.projDict:
|
||||
newWord = newWord.strip()
|
||||
self.projDict.append(newWord)
|
||||
try:
|
||||
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
|
||||
outFile.write("%s\n" % newWord)
|
||||
self.projDict.append(newWord)
|
||||
except Exception as e:
|
||||
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
|
||||
logger.error(str(e))
|
||||
return
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def listDictionaries(self):
|
||||
"""Dummy function.
|
||||
@@ -109,9 +110,12 @@ class NWSpellCheck():
|
||||
"""
|
||||
self.projDict = []
|
||||
if projectDict is not None:
|
||||
self.projectDict = projectDict
|
||||
if not os.path.isfile(projectDict):
|
||||
return
|
||||
self.projectDict = None
|
||||
return False
|
||||
else:
|
||||
self.projectDict = projectDict
|
||||
|
||||
try:
|
||||
logger.debug("Loading project word list")
|
||||
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
|
||||
@@ -123,7 +127,9 @@ class NWSpellCheck():
|
||||
except Exception as e:
|
||||
logger.error("Failed to load project word list")
|
||||
logger.error(str(e))
|
||||
return
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class NWSpellCheck
|
||||
|
||||
@@ -287,7 +293,7 @@ class NWSpellSimple(NWSpellCheck):
|
||||
if len(theWord) == 0:
|
||||
return []
|
||||
|
||||
theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
|
||||
theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
|
||||
theOptions = []
|
||||
for aWord in theMatches:
|
||||
if len(aWord) == 0:
|
||||
@@ -314,14 +320,12 @@ class NWSpellSimple(NWSpellCheck):
|
||||
retList = []
|
||||
for dictFile in os.listdir(self.mainConf.dictPath):
|
||||
|
||||
theBits = os.path.splitext(dictFile)
|
||||
if len(theBits) != 2:
|
||||
continue
|
||||
if theBits[1] != ".dict":
|
||||
fRoot, fExt = os.path.splitext(dictFile)
|
||||
if fExt != ".dict":
|
||||
continue
|
||||
|
||||
spName = "%s [%s]" % (self.expandLanguage(theBits[0]), nwConst.SP_INTERNAL)
|
||||
retList.append((theBits[0], spName))
|
||||
spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL)
|
||||
retList.append((fRoot, spName))
|
||||
|
||||
return retList
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class NWStatus():
|
||||
theStatus = checkInt(theStatus, 0, False)
|
||||
if theStatus >= 0 and theStatus < self._theLength:
|
||||
return self._theLabels[theStatus]
|
||||
return self._theLabels[0]
|
||||
|
||||
def setNewEntries(self, newList):
|
||||
"""Update the list of entries after they have been modified by
|
||||
@@ -108,7 +109,8 @@ class NWStatus():
|
||||
return
|
||||
|
||||
def countEntry(self, theLabel):
|
||||
"""Lookup the usage count of a given entry.
|
||||
"""Increment the counter for a given label. This should be used
|
||||
together with resetCounts in a loop over project items.
|
||||
"""
|
||||
theIndex = self.lookupEntry(theLabel)
|
||||
if theIndex is not None:
|
||||
@@ -136,18 +138,9 @@ class NWStatus():
|
||||
|
||||
for xChild in xParent:
|
||||
theLabels.append(xChild.text)
|
||||
if "red" in xChild.attrib:
|
||||
cR = checkInt(xChild.attrib["red"], 0, False)
|
||||
else:
|
||||
cR = 0
|
||||
if "green" in xChild.attrib:
|
||||
cG = checkInt(xChild.attrib["green"], 0, False)
|
||||
else:
|
||||
cG = 0
|
||||
if "blue" in xChild.attrib:
|
||||
cB = checkInt(xChild.attrib["blue"], 0, False)
|
||||
else:
|
||||
cB = 0
|
||||
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
|
||||
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
|
||||
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
|
||||
theColours.append((cR, cG, cB))
|
||||
|
||||
if len(theLabels) > 0:
|
||||
|
||||
@@ -68,16 +68,16 @@ class ToHtml(Tokenizer):
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setPreview(self, forPreview, doComments, doSynopsis):
|
||||
def setPreview(self, doComments, doSynopsis):
|
||||
"""If we're using this class to generate markdown preview, we
|
||||
need to make a few changes to formatting, which is managed by
|
||||
these flags.
|
||||
"""
|
||||
if forPreview:
|
||||
self.genMode = self.M_PREVIEW
|
||||
self.doKeywords = True
|
||||
self.doComments = doComments
|
||||
self.doSynopsis = doSynopsis
|
||||
self.genMode = self.M_PREVIEW
|
||||
self.doKeywords = True
|
||||
self.doComments = doComments
|
||||
self.doSynopsis = doSynopsis
|
||||
|
||||
return
|
||||
|
||||
def setStyles(self, cssStyles):
|
||||
@@ -141,11 +141,13 @@ class ToHtml(Tokenizer):
|
||||
# For novel files for export, we bump the titles one level
|
||||
# up as this is more useful for printing and word processor
|
||||
# imports.
|
||||
h1 = "h1 class='title'"
|
||||
h1Cl = " class='title'"
|
||||
h1 = "h1"
|
||||
h2 = "h1"
|
||||
h3 = "h2"
|
||||
h4 = "h3"
|
||||
else:
|
||||
h1Cl = ""
|
||||
h1 = "h1"
|
||||
h2 = "h2"
|
||||
h3 = "h3"
|
||||
@@ -193,7 +195,7 @@ class ToHtml(Tokenizer):
|
||||
else:
|
||||
aNm = ""
|
||||
|
||||
# Process TextType
|
||||
# Process Text Type
|
||||
if tType == self.T_EMPTY:
|
||||
if parStyle is None:
|
||||
parStyle = ""
|
||||
@@ -214,7 +216,7 @@ class ToHtml(Tokenizer):
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
tHead = tText.replace(r"\\", "<br/>")
|
||||
tmpResult.append("<%s%s>%s%s</%s>\n" % (h1, hStyle, aNm, tHead, h1))
|
||||
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
tHead = tText.replace(r"\\", "<br/>")
|
||||
|
||||
@@ -180,7 +180,7 @@ class Tokenizer():
|
||||
##
|
||||
|
||||
def addRootHeading(self, theHandle):
|
||||
"""Add a heading at the start if a new root folder.
|
||||
"""Add a heading at the start of a new root folder.
|
||||
"""
|
||||
theItem = self.theProject.projTree[theHandle]
|
||||
if theItem is None:
|
||||
@@ -205,7 +205,7 @@ class Tokenizer():
|
||||
self.theHandle = theHandle
|
||||
self.theItem = self.theProject.projTree[theHandle]
|
||||
if self.theItem is None:
|
||||
return
|
||||
return False
|
||||
|
||||
if theText is not None:
|
||||
# If the text is set, just use that
|
||||
@@ -234,7 +234,7 @@ class Tokenizer():
|
||||
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
|
||||
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def getResult(self):
|
||||
"""Return the result from the conversion.
|
||||
@@ -244,6 +244,8 @@ class Tokenizer():
|
||||
def getResultSize(self):
|
||||
"""Return the size of the result from the conversion.
|
||||
"""
|
||||
if self.theResult is None:
|
||||
return 0
|
||||
return len(self.theResult)
|
||||
|
||||
def getFilteredMarkdown(self):
|
||||
@@ -445,7 +447,7 @@ class Tokenizer():
|
||||
"""
|
||||
# No special header formatting for notes and no-layout files
|
||||
if self.isNone or self.isNote:
|
||||
return
|
||||
return False
|
||||
|
||||
# For novel files, we need to handle chapter numbering, scene
|
||||
# numbering, and scene breaks
|
||||
@@ -479,8 +481,7 @@ class Tokenizer():
|
||||
if self.isUnNum:
|
||||
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
|
||||
elif tToken[2].startswith("*"):
|
||||
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
|
||||
tTemp = tTemp[1:].lstrip()
|
||||
tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip())
|
||||
else:
|
||||
self.numChapter += 1
|
||||
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
|
||||
@@ -663,7 +664,7 @@ class Tokenizer():
|
||||
self.A_LEFT
|
||||
)
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
|
||||
@@ -141,6 +141,15 @@ def _numberToWordEN(numVal):
|
||||
tenWord = ""
|
||||
hunWord = ""
|
||||
|
||||
if not isinstance(numVal, int):
|
||||
return "[NaN]"
|
||||
|
||||
if numVal < 0:
|
||||
return "[Negative]"
|
||||
|
||||
if numVal > 999:
|
||||
return "[Out of Range]"
|
||||
|
||||
if numVal == 0:
|
||||
return "Zero"
|
||||
|
||||
@@ -166,19 +175,20 @@ def _numberToWordEN(numVal):
|
||||
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
|
||||
}
|
||||
|
||||
retVale = ""
|
||||
hunWord = theHundreds.get(hunVal, "")
|
||||
if tenVal == 10:
|
||||
oneWord = theTeens.get(oneVal, "")
|
||||
return f"{hunWord} {oneWord}".strip()
|
||||
retVale = f"{hunWord} {oneWord}".strip()
|
||||
else:
|
||||
oneWord = theOnes.get(oneVal, "")
|
||||
if tenVal == 0:
|
||||
return f"{hunWord} {oneWord}".strip()
|
||||
retVale = f"{hunWord} {oneWord}".strip()
|
||||
else:
|
||||
tenWord = theTens.get(tenVal, "")
|
||||
if oneVal == 0:
|
||||
return f"{hunWord} {tenWord}".strip()
|
||||
retVale = f"{hunWord} {tenWord}".strip()
|
||||
else:
|
||||
return f"{hunWord} {tenWord}-{oneWord}".strip()
|
||||
retVale = f"{hunWord} {tenWord}-{oneWord}".strip()
|
||||
|
||||
return ""
|
||||
return retVale
|
||||
|
||||
@@ -48,8 +48,10 @@ class NWTree():
|
||||
self._treeOrder = [] # The order of the tree items on the tree view
|
||||
self._treeRoots = [] # The root items of the tree
|
||||
self._trashRoot = None # The handle of the trash root folder
|
||||
self._archRoot = None # The handle of the archive root folder
|
||||
self._theIndex = 0 # The current iterator index
|
||||
self._treeChanged = False # True if tree structure has changed
|
||||
|
||||
self._handleSeed = None # Used for generating handles for testing
|
||||
|
||||
return
|
||||
@@ -83,14 +85,15 @@ class NWTree():
|
||||
if tHandle is None:
|
||||
tHandle = self._makeHandle()
|
||||
|
||||
if tHandle in self._projTree:
|
||||
logger.warning("Duplicate handle %s detected, skipping" % tHandle)
|
||||
return False
|
||||
|
||||
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
|
||||
|
||||
nwItem.setHandle(tHandle)
|
||||
nwItem.setParent(pHandle)
|
||||
|
||||
self._projTree[tHandle] = nwItem
|
||||
self._treeOrder.append(tHandle)
|
||||
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
logger.verbose("Item %s is a root item" % str(tHandle))
|
||||
self._treeRoots.append(tHandle)
|
||||
@@ -104,10 +107,13 @@ class NWTree():
|
||||
self._trashRoot = tHandle
|
||||
else:
|
||||
logger.error("Only one trash folder allowed")
|
||||
return False
|
||||
|
||||
self._projTree[tHandle] = nwItem
|
||||
self._treeOrder.append(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def packXML(self, xParent):
|
||||
"""Pack the content of the tree into the provided XML object. In
|
||||
@@ -176,8 +182,9 @@ class NWTree():
|
||||
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
return False
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def sumWords(self):
|
||||
"""Loops over all entries and adds up the word counts.
|
||||
@@ -244,6 +251,8 @@ class NWTree():
|
||||
return True
|
||||
for aRoot in self._treeRoots:
|
||||
tItem = self.__getitem__(aRoot)
|
||||
if tItem is None:
|
||||
continue
|
||||
if theClass == tItem.itemClass:
|
||||
return False
|
||||
return True
|
||||
@@ -397,7 +406,7 @@ class NWTree():
|
||||
del self._projTree[tHandle]
|
||||
else:
|
||||
logger.warning("Failed to delete item %s: item not found" % tHandle)
|
||||
return False
|
||||
return
|
||||
|
||||
if tHandle in self._treeRoots:
|
||||
self._treeRoots.remove(tHandle)
|
||||
@@ -408,7 +417,7 @@ class NWTree():
|
||||
|
||||
self._setTreeChanged(True)
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
def __contains__(self, tHandle):
|
||||
"""Checks if a handle exists in the tree.
|
||||
|
||||
@@ -135,10 +135,9 @@ class NWErrorMessage(QDialog):
|
||||
# END Class NWErrorMessage
|
||||
|
||||
|
||||
def exceptionHandler(exType, exValue, exTrace, testMode=False):
|
||||
def exceptionHandler(exType, exValue, exTrace):
|
||||
"""Function to catch unhandled global exceptions.
|
||||
"""
|
||||
import nw
|
||||
import logging
|
||||
from traceback import print_tb
|
||||
from PyQt5.QtWidgets import qApp
|
||||
@@ -160,8 +159,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
|
||||
|
||||
errMsg = NWErrorMessage(nwGUI)
|
||||
errMsg.setMessage(exType, exValue, exTrace)
|
||||
if nw.CONFIG.showGUI:
|
||||
errMsg.exec_()
|
||||
errMsg.exec_()
|
||||
|
||||
try:
|
||||
# Try a controlled shudown
|
||||
@@ -173,10 +171,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
|
||||
logger.critical("Could not close the project before exiting")
|
||||
logger.critical(str(e))
|
||||
|
||||
if testMode:
|
||||
return errMsg.msgBody.toPlainText()
|
||||
else:
|
||||
qApp.exit(1)
|
||||
qApp.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(str(e))
|
||||
|
||||
@@ -32,8 +32,9 @@ import os
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QCursor
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
|
||||
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
|
||||
QTextBrowser, QLabel
|
||||
)
|
||||
|
||||
@@ -80,6 +81,10 @@ class GuiAbout(QDialog):
|
||||
self.pageAbout.setOpenExternalLinks(True)
|
||||
self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
|
||||
|
||||
self.pageNotes = QTextBrowser()
|
||||
self.pageNotes.setOpenExternalLinks(True)
|
||||
self.pageNotes.document().setDocumentMargin(self.mainConf.pxInt(16))
|
||||
|
||||
self.pageLicense = QTextBrowser()
|
||||
self.pageLicense.setOpenExternalLinks(True)
|
||||
self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
|
||||
@@ -87,6 +92,7 @@ class GuiAbout(QDialog):
|
||||
# Main Tab Area
|
||||
self.tabBox = QTabWidget()
|
||||
self.tabBox.addTab(self.pageAbout, "About")
|
||||
self.tabBox.addTab(self.pageNotes, "Release")
|
||||
self.tabBox.addTab(self.pageLicense, "License")
|
||||
self.innerBox.addWidget(self.tabBox)
|
||||
|
||||
@@ -98,14 +104,27 @@ class GuiAbout(QDialog):
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self._setStyleSheet()
|
||||
self._fillAboutPage()
|
||||
self._fillLicensePage()
|
||||
|
||||
logger.debug("GuiAbout initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def populateGUI(self):
|
||||
"""Populate tabs with text.
|
||||
"""
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
self._setStyleSheet()
|
||||
self._fillAboutPage()
|
||||
self._fillNotesPage()
|
||||
self._fillLicensePage()
|
||||
qApp.restoreOverrideCursor()
|
||||
return
|
||||
|
||||
def showReleaseNotes(self):
|
||||
"""Show the release notes.
|
||||
"""
|
||||
self.tabBox.setCurrentWidget(self.pageNotes)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
@@ -121,14 +140,14 @@ class GuiAbout(QDialog):
|
||||
"<p>novelWriter is a markdown-like text editor designed for "
|
||||
"organising and writing novels. It is written in Python 3 with a "
|
||||
"Qt5 GUI, using PyQt5.</p>"
|
||||
"<p>novelWriter is free software: you can redistribute it and/or "
|
||||
"<p>novelWriter is free software: you can redistribute it and "
|
||||
"modify it under the terms of the GNU General Public License as "
|
||||
"published by the Free Software Foundation, either version 3 of "
|
||||
"the License, or (at your option) any later version.</p>"
|
||||
"<p>novelWriter is distributed in the hope that it will be useful, "
|
||||
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
|
||||
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
|
||||
"<p>See the License tab for the full text, or visit the GNU website "
|
||||
"<p>See the License tab for the full license text, or visit the GNU website "
|
||||
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
|
||||
"for more details.</p>"
|
||||
"<h3>Credits</h3>"
|
||||
@@ -192,11 +211,22 @@ class GuiAbout(QDialog):
|
||||
|
||||
return
|
||||
|
||||
def _fillNotesPage(self):
|
||||
"""Load the content for the Release Notes page.
|
||||
"""
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
|
||||
if os.path.isfile(docPath):
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
helpText = inFile.read()
|
||||
self.pageNotes.setHtml(helpText)
|
||||
else:
|
||||
self.pageNotes.setHtml("Error loading release notes text ...")
|
||||
return
|
||||
|
||||
def _fillLicensePage(self):
|
||||
"""Load the content for the License page.
|
||||
"""
|
||||
docName = "gplv3_%s.htm" % self.mainConf.guiLang
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", docName)
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
|
||||
if os.path.isfile(docPath):
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
helpText = inFile.read()
|
||||
@@ -221,6 +251,7 @@ class GuiAbout(QDialog):
|
||||
hColB = self.theParent.theTheme.colHead[2],
|
||||
)
|
||||
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
|
||||
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
|
||||
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
|
||||
|
||||
return
|
||||
|
||||
@@ -262,10 +262,10 @@ class GuiDocEditor(QTextEdit):
|
||||
# If we have a document open, we should reload it in case the
|
||||
# font changed, otherwise we just clear the editor entirely,
|
||||
# which makes it read only.
|
||||
if self.theHandle is not None:
|
||||
self.redrawText()
|
||||
else:
|
||||
if self.theHandle is None:
|
||||
self.clearEditor()
|
||||
else:
|
||||
self.redrawText()
|
||||
|
||||
return True
|
||||
|
||||
@@ -355,6 +355,7 @@ class GuiDocEditor(QTextEdit):
|
||||
"""Redraw the text by marking the document content as "dirty".
|
||||
"""
|
||||
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def replaceText(self, theText):
|
||||
|
||||
@@ -149,7 +149,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
# If we have a document open, we should reload it in case the font changed
|
||||
if self.theHandle is not None:
|
||||
self.redrawText()
|
||||
self.reloadText()
|
||||
|
||||
return True
|
||||
|
||||
@@ -169,7 +169,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
sPos = self.verticalScrollBar().value()
|
||||
aDoc = ToHtml(self.theProject, self.theParent)
|
||||
aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis)
|
||||
aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis)
|
||||
aDoc.setLinkHeaders(True)
|
||||
|
||||
# Be extra careful here to prevent crashes when first opening a
|
||||
@@ -233,6 +233,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Redraw the text by marking the document content as "dirty".
|
||||
"""
|
||||
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def loadFromTag(self, theTag):
|
||||
|
||||
@@ -598,8 +598,8 @@ class GuiMainMenu(QMenuBar):
|
||||
# Insert > Separator
|
||||
self.insertMenu.addSeparator()
|
||||
|
||||
# Insert > Keywords and Tags
|
||||
self.mInsKeywords = self.insertMenu.addMenu("Keywords and Tags")
|
||||
# Insert > Tags and References
|
||||
self.mInsKeywords = self.insertMenu.addMenu("Tags and References")
|
||||
self.mInsKWItems = {}
|
||||
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
|
||||
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
|
||||
@@ -908,7 +908,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.helpMenu.addAction(self.aHelpLoc)
|
||||
|
||||
self.aHelpWeb = QAction("Documentation (Online)", self)
|
||||
self.aHelpWeb.setStatusTip("View online documentation")
|
||||
self.aHelpWeb.setStatusTip("View online documentation at %s" % nw.__docurl__)
|
||||
self.aHelpWeb.triggered.connect(lambda: self._openWebsite(nw.__docurl__))
|
||||
if self.mainConf.hasHelp and self.mainConf.hasAssistant:
|
||||
self.aHelpWeb.setShortcut("Shift+F1")
|
||||
@@ -916,18 +916,33 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aHelpWeb.setShortcuts(["F1", "Shift+F1"])
|
||||
self.helpMenu.addAction(self.aHelpWeb)
|
||||
|
||||
# Document > Go to Website
|
||||
self.aWebsite = QAction("Open the novelWriter Website", self)
|
||||
self.aWebsite.setStatusTip("View the main website")
|
||||
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
|
||||
self.helpMenu.addAction(self.aWebsite)
|
||||
# Help > Separator
|
||||
self.helpMenu.addSeparator()
|
||||
|
||||
# Document > Report Issue
|
||||
self.aIssue = QAction("Report an Issue", self)
|
||||
self.aIssue.setStatusTip("Report a bug or issue on GitHub")
|
||||
# Document > Report an Issue
|
||||
self.aIssue = QAction("Report an Issue (GitHub)", self)
|
||||
self.aIssue.setStatusTip("Report a bug or issue on GitHub at %s" % nw.__issuesurl__)
|
||||
self.aIssue.triggered.connect(lambda: self._openWebsite(nw.__issuesurl__))
|
||||
self.helpMenu.addAction(self.aIssue)
|
||||
|
||||
# Document > Ask a Question
|
||||
self.aQuestion = QAction("Ask a Question (GitHub)", self)
|
||||
self.aQuestion.setStatusTip("Ask a question on GitHub at %s" % nw.__helpurl__)
|
||||
self.aQuestion.triggered.connect(lambda: self._openWebsite(nw.__helpurl__))
|
||||
self.helpMenu.addAction(self.aQuestion)
|
||||
|
||||
# Document > Latest Release
|
||||
self.aRelease = QAction("Latest Release (GitHub)", self)
|
||||
self.aRelease.setStatusTip("Open the Releases page on GitHub at %s" % nw.__releaseurl__)
|
||||
self.aRelease.triggered.connect(lambda: self._openWebsite(nw.__releaseurl__))
|
||||
self.helpMenu.addAction(self.aRelease)
|
||||
|
||||
# Document > Main Website
|
||||
self.aWebsite = QAction("The novelWriter Website", self)
|
||||
self.aWebsite.setStatusTip("Open the novelWriter website at %s" % nw.__url__)
|
||||
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
|
||||
self.helpMenu.addAction(self.aWebsite)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiMainMenu
|
||||
|
||||
@@ -227,7 +227,7 @@ class GuiConfigEditGeneralTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Show full path in document header",
|
||||
self.showFullPath,
|
||||
"Shows the document title and parent folder names."
|
||||
"Add the parent folder names to the header."
|
||||
)
|
||||
|
||||
self.hideVScroll = QSwitch()
|
||||
@@ -235,7 +235,7 @@ class GuiConfigEditGeneralTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Hide vertical scroll bars in main windows",
|
||||
self.hideVScroll,
|
||||
"Scrolling with mouse wheel and keys only."
|
||||
"Scrolling available with mouse wheel and keys only."
|
||||
)
|
||||
|
||||
self.hideHScroll = QSwitch()
|
||||
@@ -243,7 +243,7 @@ class GuiConfigEditGeneralTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Hide horizontal scroll bars in main windows",
|
||||
self.hideHScroll,
|
||||
"Scrolling with mouse wheel and keys only."
|
||||
"Scrolling available with mouse wheel and keys only."
|
||||
)
|
||||
|
||||
return
|
||||
@@ -325,8 +325,9 @@ class GuiConfigEditProjectsTab(QWidget):
|
||||
self.autoSaveDoc.setSingleStep(1)
|
||||
self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc)
|
||||
self.backupPathRow = self.mainForm.addRow(
|
||||
"Save interval for the currently open document",
|
||||
"Save document interval",
|
||||
self.autoSaveDoc,
|
||||
"How often the open document is automatically saved.",
|
||||
theUnit="seconds"
|
||||
)
|
||||
|
||||
@@ -337,8 +338,9 @@ class GuiConfigEditProjectsTab(QWidget):
|
||||
self.autoSaveProj.setSingleStep(1)
|
||||
self.autoSaveProj.setValue(self.mainConf.autoSaveProj)
|
||||
self.backupPathRow = self.mainForm.addRow(
|
||||
"Save interval for the currently open project",
|
||||
"Save project interval",
|
||||
self.autoSaveProj,
|
||||
"How often the open project is automatically saved.",
|
||||
theUnit="seconds"
|
||||
)
|
||||
|
||||
@@ -361,9 +363,9 @@ class GuiConfigEditProjectsTab(QWidget):
|
||||
self.backupOnClose.setChecked(self.mainConf.backupOnClose)
|
||||
self.backupOnClose.toggled.connect(self._toggledBackupOnClose)
|
||||
self.mainForm.addRow(
|
||||
"Run backup when closing project",
|
||||
"Run backup when the project is closed",
|
||||
self.backupOnClose,
|
||||
"This option can be overridden in project settings."
|
||||
"Can be overridden for individual projects in project settings."
|
||||
)
|
||||
|
||||
## Ask before backup
|
||||
@@ -373,7 +375,8 @@ class GuiConfigEditProjectsTab(QWidget):
|
||||
self.askBeforeBackup.setEnabled(self.mainConf.backupOnClose)
|
||||
self.mainForm.addRow(
|
||||
"Ask before running backup",
|
||||
self.askBeforeBackup
|
||||
self.askBeforeBackup,
|
||||
"Disabling this will cause backups to run in the background."
|
||||
)
|
||||
|
||||
return
|
||||
@@ -426,7 +429,7 @@ class GuiConfigEditProjectsTab(QWidget):
|
||||
|
||||
def _toggledBackupOnClose(self, theState):
|
||||
"""Enable or disable switch that depends on the backup on close
|
||||
switch,
|
||||
switch.
|
||||
"""
|
||||
self.askBeforeBackup.setEnabled(theState)
|
||||
return
|
||||
@@ -449,7 +452,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
|
||||
# Text Style
|
||||
# ==========
|
||||
self.mainForm.addGroupLabel("Text Style")
|
||||
self.mainForm.addGroupLabel("Document Text Style")
|
||||
|
||||
## Font Family
|
||||
self.textStyleFont = QLineEdit()
|
||||
@@ -475,12 +478,13 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Font size",
|
||||
self.textStyleSize,
|
||||
"Font size for the document editor and viewer.",
|
||||
theUnit = "pt"
|
||||
)
|
||||
|
||||
# Text Flow
|
||||
# =========
|
||||
self.mainForm.addGroupLabel("Text Flow")
|
||||
self.mainForm.addGroupLabel("Document Text Flow")
|
||||
|
||||
## Max Text Width in Normal Mode
|
||||
self.textFlowMax = QSpinBox(self)
|
||||
@@ -491,6 +495,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Maximum text width in \"Normal Mode\"",
|
||||
self.textFlowMax,
|
||||
"Horizontal margins are scaled automatically.",
|
||||
theUnit="px"
|
||||
)
|
||||
|
||||
@@ -503,6 +508,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Maximum text width in \"Focus Mode\"",
|
||||
self.focusDocWidth,
|
||||
"Horizontal margins are scaled automatically.",
|
||||
theUnit="px"
|
||||
)
|
||||
|
||||
@@ -512,7 +518,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Disable maximum text width in \"Normal Mode\"",
|
||||
self.textFlowFixed,
|
||||
"If disabled, minimum text width is defined by the margin setting."
|
||||
"If disabled, minimum text width is defined by the margin."
|
||||
)
|
||||
|
||||
## Focus Mode Footer
|
||||
@@ -520,7 +526,8 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.hideFocusFooter.setChecked(self.mainConf.hideFocusFooter)
|
||||
self.mainForm.addRow(
|
||||
"Hide document footer in \"Focus Mode\"",
|
||||
self.hideFocusFooter
|
||||
self.hideFocusFooter,
|
||||
"Hide the information bar at the bottom of the document."
|
||||
)
|
||||
|
||||
## Justify Text
|
||||
@@ -528,7 +535,8 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.textJustify.setChecked(self.mainConf.textFixedW)
|
||||
self.mainForm.addRow(
|
||||
"Justify the text margins in editor and viewer",
|
||||
self.textJustify
|
||||
self.textJustify,
|
||||
"Lay out text with straight edges in the editor and viewer."
|
||||
)
|
||||
|
||||
## Document Margins
|
||||
@@ -538,9 +546,9 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.textMargin.setSingleStep(1)
|
||||
self.textMargin.setValue(self.mainConf.textMargin)
|
||||
self.mainForm.addRow(
|
||||
"Document text margin",
|
||||
"Text margin",
|
||||
self.textMargin,
|
||||
"If max width is enabled, this is the minimum margin.",
|
||||
"If maximum width is set, this becomes the minimum margin.",
|
||||
theUnit="px"
|
||||
)
|
||||
|
||||
@@ -551,8 +559,9 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.tabWidth.setSingleStep(1)
|
||||
self.tabWidth.setValue(self.mainConf.tabWidth)
|
||||
self.mainForm.addRow(
|
||||
"Document tab width",
|
||||
"Tab width",
|
||||
self.tabWidth,
|
||||
"The width of a tab key press in the editor and viewer.",
|
||||
theUnit="px"
|
||||
)
|
||||
|
||||
@@ -566,7 +575,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Scroll past end of the document",
|
||||
self.scrollPastEnd,
|
||||
"Allows scrolling until last line is at the top."
|
||||
"Allow scrolling until the last line is centred in the editor."
|
||||
)
|
||||
|
||||
## Typewriter Scrolling
|
||||
@@ -575,7 +584,7 @@ class GuiConfigEditLayoutTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Typewriter style scrolling when you type",
|
||||
self.autoScroll,
|
||||
"Tries to keep the cursor at a fixed vertical position."
|
||||
"Try to keep the cursor at a fixed vertical position."
|
||||
)
|
||||
|
||||
## Font Size
|
||||
@@ -678,7 +687,7 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Highlight theme",
|
||||
self.selectSyntax,
|
||||
""
|
||||
"Colour theme to apply to the editor and viewer."
|
||||
)
|
||||
|
||||
self.highlightQuotes = QSwitch()
|
||||
@@ -686,7 +695,7 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Highlight text wrapped in quotes",
|
||||
self.highlightQuotes,
|
||||
helpText="Applies to single, double and straight quotes."
|
||||
"Applies to single, double and straight quotes."
|
||||
)
|
||||
|
||||
self.highlightEmph = QSwitch()
|
||||
@@ -694,7 +703,7 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Add highlight colour to emphasised text",
|
||||
self.highlightEmph,
|
||||
helpText="Applies to emphasis, strong and strikethrough."
|
||||
"Applies to emphasis, strong and strikethrough."
|
||||
)
|
||||
|
||||
# Spell Checking
|
||||
@@ -724,7 +733,8 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
)
|
||||
self.mainForm.addRow(
|
||||
"Spell check language",
|
||||
self.spellLangList
|
||||
self.spellLangList,
|
||||
"Available languages are determined by your system."
|
||||
)
|
||||
|
||||
## Big Document Size Limit
|
||||
@@ -736,7 +746,7 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Big document limit",
|
||||
self.bigDocLimit,
|
||||
"Disables full spell checking over the size limit.",
|
||||
"Full spell checking is disabled above this limit.",
|
||||
theUnit="kB"
|
||||
)
|
||||
|
||||
@@ -749,7 +759,8 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
self.showTabsNSpaces.setChecked(self.mainConf.showTabsNSpaces)
|
||||
self.mainForm.addRow(
|
||||
"Show tabs and spaces",
|
||||
self.showTabsNSpaces
|
||||
self.showTabsNSpaces,
|
||||
"Add symbols to indicate tabs and spaces in the editor."
|
||||
)
|
||||
|
||||
## Show Line Endings
|
||||
@@ -757,7 +768,8 @@ class GuiConfigEditEditingTab(QWidget):
|
||||
self.showLineEndings.setChecked(self.mainConf.showLineEndings)
|
||||
self.mainForm.addRow(
|
||||
"Show line endings",
|
||||
self.showLineEndings
|
||||
self.showLineEndings,
|
||||
"Add a symbol to indicate line endings in the editor."
|
||||
)
|
||||
|
||||
return
|
||||
@@ -866,7 +878,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Auto-replace text as you type",
|
||||
self.autoReplaceMain,
|
||||
"Apply formatting to word under cursor if no selection is made."
|
||||
"Allow the editor to replace symbols as you type."
|
||||
)
|
||||
|
||||
# Auto-Replace
|
||||
@@ -880,7 +892,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Auto-replace single quotes",
|
||||
self.autoReplaceSQ,
|
||||
"The feature will try to guess opening or closing single quote."
|
||||
"Try to guess which is an opening or a closing single quote."
|
||||
)
|
||||
|
||||
## Auto-Replace Double Quotes
|
||||
@@ -890,7 +902,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Auto-replace double quotes",
|
||||
self.autoReplaceDQ,
|
||||
"The feature will try to guess opening or closing quote quote."
|
||||
"Try to guess which is an opening or a closing double quote."
|
||||
)
|
||||
|
||||
## Auto-Replace Hyphens
|
||||
@@ -900,7 +912,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Auto-replace dashes",
|
||||
self.autoReplaceDash,
|
||||
"Auto-replace double and triple hyphens with short and long dash."
|
||||
"Double and triple hyphens become short and long dashes."
|
||||
)
|
||||
|
||||
## Auto-Replace Dots
|
||||
@@ -910,7 +922,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Auto-replace dots",
|
||||
self.autoReplaceDots,
|
||||
"Auto-replace three dots with ellipsis."
|
||||
"Three consecutive dots becomes ellipsis."
|
||||
)
|
||||
|
||||
# Quotation Style
|
||||
@@ -934,7 +946,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Single quote open style",
|
||||
self.quoteSym["SO"],
|
||||
"Auto-replaces apostrophe before words.",
|
||||
"The symbol to use for a leading single quote.",
|
||||
theButton=self.btnSingleStyleO
|
||||
)
|
||||
|
||||
@@ -950,7 +962,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Single quote close style",
|
||||
self.quoteSym["SC"],
|
||||
"Auto-replaces apostrophe after words.",
|
||||
"The symbol to use for a trailing single quote.",
|
||||
theButton=self.btnSingleStyleC
|
||||
)
|
||||
|
||||
@@ -967,7 +979,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Double quote open style",
|
||||
self.quoteSym["DO"],
|
||||
"Auto-replaces straight quotes before words.",
|
||||
"The symbol to use for a leading double quote.",
|
||||
theButton=self.btnDoubleStyleO
|
||||
)
|
||||
|
||||
@@ -983,7 +995,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
|
||||
self.mainForm.addRow(
|
||||
"Double quote close style",
|
||||
self.quoteSym["DC"],
|
||||
"Auto-replaces straight quotes after words.",
|
||||
"The symbol to use for a trailing double quote.",
|
||||
theButton=self.btnDoubleStyleC
|
||||
)
|
||||
|
||||
|
||||
@@ -286,36 +286,36 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
|
||||
if hasFocus and self.theParent.hasProject:
|
||||
if qApp.focusWidget() != self and self.mainConf.showGUI:
|
||||
return False
|
||||
|
||||
tHandle = self.getSelectedHandle()
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
pItem = tItem.parent()
|
||||
if pItem is None:
|
||||
tIndex = self.indexOfTopLevelItem(tItem)
|
||||
nChild = self.topLevelItemCount()
|
||||
nIndex = tIndex + nStep
|
||||
if nIndex < 0 or nIndex >= nChild:
|
||||
return False
|
||||
cItem = self.takeTopLevelItem(tIndex)
|
||||
self.insertTopLevelItem(nIndex, cItem)
|
||||
tHandle = self.getSelectedHandle()
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
if tItem is None:
|
||||
return False
|
||||
|
||||
else:
|
||||
tIndex = pItem.indexOfChild(tItem)
|
||||
nChild = pItem.childCount()
|
||||
nIndex = tIndex + nStep
|
||||
if nIndex < 0 or nIndex >= nChild:
|
||||
return False
|
||||
cItem = pItem.takeChild(tIndex)
|
||||
pItem.insertChild(nIndex, cItem)
|
||||
|
||||
self.clearSelection()
|
||||
cItem.setSelected(True)
|
||||
self._setTreeChanged(True)
|
||||
pItem = tItem.parent()
|
||||
if pItem is None:
|
||||
tIndex = self.indexOfTopLevelItem(tItem)
|
||||
nChild = self.topLevelItemCount()
|
||||
nIndex = tIndex + nStep
|
||||
if nIndex < 0 or nIndex >= nChild:
|
||||
return False
|
||||
cItem = self.takeTopLevelItem(tIndex)
|
||||
self.insertTopLevelItem(nIndex, cItem)
|
||||
|
||||
else:
|
||||
return False
|
||||
tIndex = pItem.indexOfChild(tItem)
|
||||
nChild = pItem.childCount()
|
||||
nIndex = tIndex + nStep
|
||||
if nIndex < 0 or nIndex >= nChild:
|
||||
return False
|
||||
cItem = pItem.takeChild(tIndex)
|
||||
pItem.insertChild(nIndex, cItem)
|
||||
|
||||
self.clearSelection()
|
||||
cItem.setSelected(True)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -431,7 +431,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
trItemS = self._getTreeItem(tHandle)
|
||||
nwItemS = self.theProject.projTree[tHandle]
|
||||
|
||||
if nwItemS is None:
|
||||
if trItemS is None or nwItemS is None:
|
||||
return False
|
||||
|
||||
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
|
||||
@@ -582,18 +582,23 @@ class GuiProjectTree(QTreeWidget):
|
||||
properly reported to the function.
|
||||
"""
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
if tItem is not None:
|
||||
tItem.setText(self.C_COUNT, f"{theCount:n}")
|
||||
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
|
||||
pItem = tItem.parent()
|
||||
if pItem is not None:
|
||||
pCount = 0
|
||||
for i in range(pItem.childCount()):
|
||||
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
|
||||
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
|
||||
if tItem is None:
|
||||
return
|
||||
|
||||
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
|
||||
self.propagateCount(pHandle, pCount, nDepth+1)
|
||||
tItem.setText(self.C_COUNT, f"{theCount:n}")
|
||||
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
|
||||
|
||||
pItem = tItem.parent()
|
||||
if pItem is None:
|
||||
return
|
||||
|
||||
pCount = 0
|
||||
for i in range(pItem.childCount()):
|
||||
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
|
||||
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
|
||||
|
||||
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
|
||||
self.propagateCount(pHandle, pCount, nDepth+1)
|
||||
|
||||
return
|
||||
|
||||
@@ -1024,12 +1029,12 @@ class GuiProjectTreeMenu(QMenu):
|
||||
return
|
||||
|
||||
def filterActions(self, theItem):
|
||||
"""Update item settings from the nwItem.
|
||||
"""Filter the menu entries available based on the properties of
|
||||
the item the menu was activated on.
|
||||
"""
|
||||
self.theItem = theItem
|
||||
theRoot = self.theTree.theProject.projTree.getRootItem(theItem.itemHandle)
|
||||
|
||||
if theItem is None or theRoot is None:
|
||||
if theItem is None:
|
||||
logger.error("Failed to extract information to build tree context menu")
|
||||
return False
|
||||
|
||||
@@ -1038,14 +1043,13 @@ class GuiProjectTreeMenu(QMenu):
|
||||
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
|
||||
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
|
||||
isFile = theItem.itemType == nwItemType.FILE
|
||||
isArch = theRoot.itemClass == nwItemClass.ARCHIVE
|
||||
isOrph = isFile and theItem.itemParent is None
|
||||
|
||||
showOpen = isFile
|
||||
showView = isFile
|
||||
showEdit = not isTrash and not isOrph
|
||||
showExport = isFile
|
||||
showNewFile = not (isTrash or inTrash or isOrph or isArch)
|
||||
showNewFile = not (isTrash or inTrash or isOrph)
|
||||
showNewFolder = not (isTrash or inTrash or isOrph)
|
||||
showDelete = not isTrash
|
||||
showEmpty = isTrash
|
||||
|
||||
@@ -281,11 +281,11 @@ class GuiTheme:
|
||||
cnfSec = "Main"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
|
||||
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
|
||||
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
|
||||
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
|
||||
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "N/A")
|
||||
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
|
||||
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
|
||||
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
|
||||
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
|
||||
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
|
||||
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
|
||||
|
||||
## Palette
|
||||
@@ -638,10 +638,10 @@ class GuiIcons:
|
||||
if confParser.has_section(cnfSec):
|
||||
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
|
||||
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
|
||||
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
|
||||
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
|
||||
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
|
||||
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
|
||||
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
|
||||
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
|
||||
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
|
||||
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
|
||||
|
||||
## Palette
|
||||
|
||||
@@ -81,14 +81,14 @@ class GuiMain(QMainWindow):
|
||||
# Core Classes
|
||||
# ============
|
||||
|
||||
# Core Classes and settings
|
||||
# Core Classes and Settings
|
||||
self.theTheme = GuiTheme(self)
|
||||
self.theProject = NWProject(self)
|
||||
self.theIndex = NWIndex(self.theProject, self)
|
||||
self.hasProject = False
|
||||
self.isFocusMode = False
|
||||
|
||||
# Prepare main window
|
||||
# Prepare Main Window
|
||||
self.resize(*self.mainConf.getWinSize())
|
||||
self._setWindowTitle()
|
||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||
@@ -238,6 +238,12 @@ class GuiMain(QMainWindow):
|
||||
if self.mainConf.showGUI:
|
||||
self.showProjectLoadDialog()
|
||||
|
||||
# Show the latest release notes, if they haven't been shown before
|
||||
if self.mainConf.lastNotes != nw.__version__:
|
||||
if self.mainConf.showGUI:
|
||||
self.showAboutNWDialog(showNotes=True)
|
||||
self.mainConf.lastNotes = nw.__version__
|
||||
|
||||
logger.debug("novelWriter is ready ...")
|
||||
self.setStatus("novelWriter is ready ...")
|
||||
|
||||
@@ -915,11 +921,18 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
def showAboutNWDialog(self):
|
||||
def showAboutNWDialog(self, showNotes=False):
|
||||
"""Show the about dialog for novelWriter.
|
||||
"""
|
||||
dlgAbout = GuiAbout(self)
|
||||
dlgAbout.exec_()
|
||||
dlgAbout.setModal(True)
|
||||
dlgAbout.show()
|
||||
qApp.processEvents()
|
||||
dlgAbout.populateGUI()
|
||||
|
||||
if showNotes:
|
||||
dlgAbout.showReleaseNotes()
|
||||
|
||||
return
|
||||
|
||||
def showAboutQtDialog(self):
|
||||
@@ -969,6 +982,13 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
def askQuestion(self, theTitle, theQuestion):
|
||||
"""Ask the user a Yes/No question.
|
||||
"""
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(self, theTitle, theQuestion)
|
||||
return msgRes == QMessageBox.Yes
|
||||
|
||||
def reportConfErr(self):
|
||||
"""Checks if the Config module has any errors to report, and let
|
||||
the user know if this is the case. The Config module caches
|
||||
@@ -1060,10 +1080,10 @@ class GuiMain(QMainWindow):
|
||||
|
||||
self.isFocusMode = not self.isFocusMode
|
||||
if self.isFocusMode:
|
||||
logger.debug("Activating Focus mode")
|
||||
logger.debug("Activating Focus Mode")
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
else:
|
||||
logger.debug("Deactivating Focus mode")
|
||||
logger.debug("Deactivating Focus Mode")
|
||||
|
||||
isVisible = not self.isFocusMode
|
||||
self.treePane.setVisible(isVisible)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[pytest]
|
||||
markers =
|
||||
project: Project classes tests
|
||||
error: Test various error handling scenarios
|
||||
base: Base functionality tests
|
||||
core: Core functionality tests
|
||||
gui: Qt5 GUI tests
|
||||
serial
|
||||
|
||||
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -27,11 +27,12 @@ UsedUserAreasWarning=no
|
||||
;PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputDir={#nwAppDir}
|
||||
OutputBaseFilename=novelwriter_{#nwAppVersion}_win_amd64_setup
|
||||
OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-setup
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ChangesAssociations=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -50,3 +51,10 @@ Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Fil
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Registry]
|
||||
Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; ValueName: "novelWriterProject.nwx"; ValueData: ""; Flags: uninsdeletevalue
|
||||
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico"
|
||||
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\novelWriter.exe"" ""%1"""
|
||||
Root: HKA; Subkey: "Software\Classes\Applications\novelWriter.exe\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: ""
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# novelWriter Tests
|
||||
|
||||
The test suite uses PyTest for testing.
|
||||
|
||||
## Dependencies
|
||||
|
||||
* `python3-pytest` for the basic framework (required)
|
||||
* `python3-pytestqt` for Qt support (required)
|
||||
* `python3-pytest-cov` for code coverage reports (optional)
|
||||
* `python3-pytest-xvfb` for headless tests (optional)
|
||||
|
||||
## HowTo
|
||||
|
||||
### Basic Usage
|
||||
|
||||
To run all tests, type:
|
||||
```bash
|
||||
pytest-3 -v
|
||||
```
|
||||
|
||||
The `-v` switch enables verbose mode, with one test per line.
|
||||
For a more compact view, omit this switch.
|
||||
|
||||
### Headless
|
||||
|
||||
To run tests in headless mode, either use the Qt `offscreen` mode:
|
||||
```bash
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
```
|
||||
|
||||
or run with `xvfb`:
|
||||
```bash
|
||||
xvfb-run pytest-3 -v
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
To add test coverage, run the following:
|
||||
```bash
|
||||
pytest-3 -v --cov=nw --cov-report=html
|
||||
```
|
||||
|
||||
The `--cov-report` switch generates an html report, omit it to print a coverage summary to the terminal.
|
||||
The html coverage report will be available in the `htmlcov` folder.
|
||||
|
||||
### Test Markers (Categories)
|
||||
|
||||
To run with specific test markers, add the `-m` switch:
|
||||
```bash
|
||||
pytest-3 -v -m core
|
||||
```
|
||||
|
||||
Available markers are:
|
||||
|
||||
* '`core`' for unit tests covering the classes in the `nw/core` folder
|
||||
|
||||
## Tests
|
||||
|
||||
To filter specific groups of tests, use the `-k` switch.
|
||||
The commands for the respective test categories are listed below.
|
||||
|
||||
| Type | Test Target | Source File(s) | Marker | Filter |
|
||||
| :--- | :----------------- | :-------------------- | :-------- | :-------------------- |
|
||||
| Unit | Main function | nw/\_\_init\_\_.py | `-m base` | `-k testBaseInit` |
|
||||
| Unit | Common functions | nw/common.py | `-m base` | `-k testBaseCommon` |
|
||||
| Unit | Config class | nw/config.py | `-m base` | `-k testBaseConfig` |
|
||||
| Unit | Error handlers | nw/error.py | `-m base` | `-k testBaseError` |
|
||||
| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` |
|
||||
| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` |
|
||||
| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` |
|
||||
| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` |
|
||||
| Unit | NWProject class | nw/core/project.py | `-m core` | `-k testCoreProject` |
|
||||
| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` |
|
||||
| Unit | NWStatus class | nw/core/status.py | `-m core` | `-k testCoreStatus` |
|
||||
| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` |
|
||||
| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` |
|
||||
| Unit | ToHtml class | nw/core/tohtml.py | `-m core` | `-k testCoreToHtml` |
|
||||
| Unit | Tokenizer class | nw/core/tokenizer.py | `-m core` | `-k testCoreToken` |
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
import shutil
|
||||
import os
|
||||
|
||||
from nwdummy import DummyMain
|
||||
from dummy import DummyMain
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
@@ -20,94 +20,42 @@ from nw.config import Config # noqa: E402
|
||||
##
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwTemp():
|
||||
def tmpDir():
|
||||
"""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 = os.path.dirname(__file__)
|
||||
tempDir = os.path.join(testDir, "temp")
|
||||
if os.path.isdir(tempDir):
|
||||
shutil.rmtree(tempDir)
|
||||
if not os.path.isdir(tempDir):
|
||||
os.mkdir(tempDir)
|
||||
return tempDir
|
||||
theDir = os.path.join(testDir, "temp")
|
||||
if os.path.isdir(theDir):
|
||||
shutil.rmtree(theDir)
|
||||
if not os.path.isdir(theDir):
|
||||
os.mkdir(theDir)
|
||||
return theDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwRef():
|
||||
def refDir():
|
||||
"""The folder where all the reference files are stored for verifying
|
||||
the results of tests.
|
||||
"""
|
||||
testDir = os.path.dirname(__file__)
|
||||
refDir = os.path.join(testDir, "reference")
|
||||
return refDir
|
||||
|
||||
##
|
||||
# novelWriter Objects
|
||||
##
|
||||
theDir = os.path.join(testDir, "reference")
|
||||
return theDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmpConf(nwTemp):
|
||||
"""Create a temporary novelWriter configuration object.
|
||||
def outDir(tmpDir):
|
||||
"""An output folder for test results
|
||||
"""
|
||||
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 = 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 = 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 = os.path.join(nwTemp, "build")
|
||||
if not os.path.isdir(buildDir):
|
||||
os.mkdir(buildDir)
|
||||
return buildDir
|
||||
theDir = os.path.join(tmpDir, "results")
|
||||
if not os.path.isdir(theDir):
|
||||
os.mkdir(theDir)
|
||||
return theDir
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def nwFuncTemp(nwTemp):
|
||||
def fncDir(tmpDir):
|
||||
"""A temporary folder for a single test function.
|
||||
"""
|
||||
funcDir = os.path.join(nwTemp, "ftemp")
|
||||
funcDir = os.path.join(tmpDir, "ftemp")
|
||||
if os.path.isdir(funcDir):
|
||||
shutil.rmtree(funcDir)
|
||||
if not os.path.isdir(funcDir):
|
||||
@@ -118,16 +66,40 @@ def nwFuncTemp(nwTemp):
|
||||
return
|
||||
|
||||
##
|
||||
# Temp Folders for Projects
|
||||
# novelWriter Objects
|
||||
##
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def nwMinimal(nwTemp):
|
||||
def tmpConf(tmpDir):
|
||||
"""Create a temporary novelWriter configuration object.
|
||||
"""
|
||||
confFile = os.path.join(tmpDir, "novelwriter.conf")
|
||||
if os.path.isfile(confFile):
|
||||
os.unlink(confFile)
|
||||
theConf = Config()
|
||||
theConf.initConfig(tmpDir, tmpDir)
|
||||
theConf.setLastPath("")
|
||||
return theConf
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def dummyGUI(tmpConf):
|
||||
"""Create a dummy instance of novelWriter's main GUI class.
|
||||
"""
|
||||
theDummy = DummyMain()
|
||||
theDummy.mainConf = tmpConf
|
||||
return theDummy
|
||||
|
||||
##
|
||||
# Temp Project Folders
|
||||
##
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def nwMinimal(tmpDir):
|
||||
"""A minimal novelWriter example project.
|
||||
"""
|
||||
testDir = os.path.dirname(__file__)
|
||||
minimalStore = os.path.join(testDir, "minimal")
|
||||
minimalDir = os.path.join(nwTemp, "minimal")
|
||||
minimalDir = os.path.join(tmpDir, "minimal")
|
||||
if os.path.isdir(minimalDir):
|
||||
shutil.rmtree(minimalDir)
|
||||
shutil.copytree(minimalStore, minimalDir)
|
||||
@@ -143,13 +115,13 @@ def nwMinimal(nwTemp):
|
||||
return
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def nwLipsum(nwTemp):
|
||||
def nwLipsum(tmpDir):
|
||||
"""A medium sized novelWriter example project with a lot of Lorem
|
||||
Ipsum dummy text.
|
||||
"""
|
||||
testDir = os.path.dirname(__file__)
|
||||
lipsumStore = os.path.join(testDir, "lipsum")
|
||||
lipsumDir = os.path.join(nwTemp, "lipsum")
|
||||
lipsumDir = os.path.join(tmpDir, "lipsum")
|
||||
if os.path.isdir(lipsumDir):
|
||||
shutil.rmtree(lipsumDir)
|
||||
shutil.copytree(lipsumStore, lipsumDir)
|
||||
@@ -165,12 +137,12 @@ def nwLipsum(nwTemp):
|
||||
return
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def nwOldProj(nwTemp):
|
||||
def nwOldProj(tmpDir):
|
||||
"""A minimal movelWriter project using the old folder structure.
|
||||
"""
|
||||
testDir = os.path.dirname(__file__)
|
||||
oldProjStore = os.path.join(testDir, "oldproj")
|
||||
oldProjDir = os.path.join(nwTemp, "oldproj")
|
||||
oldProjDir = os.path.join(tmpDir, "oldproj")
|
||||
if os.path.isdir(oldProjDir):
|
||||
shutil.rmtree(oldProjDir)
|
||||
shutil.copytree(oldProjStore, oldProjDir)
|
||||
@@ -199,4 +171,30 @@ def yesToAll(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes
|
||||
)
|
||||
yield
|
||||
monkeypatch.undo()
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
|
||||
##
|
||||
# Temporary Test Folders
|
||||
##
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwTempGUI(tmpDir):
|
||||
"""A temporary folder for GUI tests.
|
||||
"""
|
||||
guiDir = os.path.join(tmpDir, "gui")
|
||||
if not os.path.isdir(guiDir):
|
||||
os.mkdir(guiDir)
|
||||
return guiDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def nwTempBuild(tmpDir):
|
||||
"""A temporary folder for build tests.
|
||||
"""
|
||||
buildDir = os.path.join(tmpDir, "build")
|
||||
if not os.path.isdir(buildDir):
|
||||
os.mkdir(buildDir)
|
||||
return buildDir
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Test Dummy GUI Classes
|
||||
"""
|
||||
|
||||
# =========================================================================== #
|
||||
# Mock GUI
|
||||
# =========================================================================== #
|
||||
|
||||
class DummyMain():
|
||||
|
||||
def __init__(self):
|
||||
self.mainConf = None
|
||||
self.hasProject = True
|
||||
self.theIndex = None
|
||||
self.theProject = None
|
||||
self.statusBar = DummyStatusBar()
|
||||
|
||||
# Test Variables
|
||||
self.askResponse = True
|
||||
self.lastAlert = ""
|
||||
|
||||
return
|
||||
|
||||
def makeAlert(self, theMessage, theLevel):
|
||||
print("%s: %s" % (str(theLevel), theMessage))
|
||||
self.lastAlert = str(theMessage)
|
||||
return
|
||||
|
||||
def askQuestion(self, theTitle, theQustion):
|
||||
print("Question: %s" % theQustion)
|
||||
return self.askResponse
|
||||
|
||||
def setStatus(self, theMessage):
|
||||
return
|
||||
|
||||
def setProjectStatus(self, isChanged):
|
||||
return
|
||||
|
||||
def openProject(self, projPath):
|
||||
return
|
||||
|
||||
def rebuildIndex(self):
|
||||
return
|
||||
|
||||
def closeMain(self):
|
||||
return "closeMain"
|
||||
|
||||
def close(self):
|
||||
return "close"
|
||||
|
||||
# Test Functions
|
||||
|
||||
def undo(self):
|
||||
self.askResponse = True
|
||||
return
|
||||
|
||||
def clear(self):
|
||||
self.lastAlert = ""
|
||||
return
|
||||
|
||||
# END Class DummyMain
|
||||
|
||||
class DummyStatusBar():
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def setStatus(self, theText):
|
||||
return
|
||||
|
||||
# END Class DummyStatusBar
|
||||
|
||||
# =========================================================================== #
|
||||
# Error Functions
|
||||
# Dummy functions that will raise errors instead.
|
||||
# =========================================================================== #
|
||||
|
||||
def causeOSError(*args, **kwargs):
|
||||
raise OSError
|
||||
|
||||
def causeException(*args, **kwargs):
|
||||
raise Exception
|
||||
@@ -1,77 +0,0 @@
|
||||
[
|
||||
[
|
||||
"content/04468803b92e1.nwd",
|
||||
"WORLD",
|
||||
"Ancient Europe"
|
||||
],
|
||||
[
|
||||
"content/2426c6f0ca922.nwd",
|
||||
"PLOT",
|
||||
"Main"
|
||||
],
|
||||
[
|
||||
"content/441420a886d82.nwd",
|
||||
"NOVEL",
|
||||
"Chapter Two"
|
||||
],
|
||||
[
|
||||
"content/47666c91c7ccf.nwd",
|
||||
"NOVEL",
|
||||
"Scene Five"
|
||||
],
|
||||
[
|
||||
"content/4c4f28287af27.nwd",
|
||||
"CHARACTER",
|
||||
"Mr. Nobody"
|
||||
],
|
||||
[
|
||||
"content/7a992350f3eb6.nwd",
|
||||
"NOVEL",
|
||||
"Lorem Ipsum"
|
||||
],
|
||||
[
|
||||
"content/846352075de7d.nwd",
|
||||
"NOVEL",
|
||||
"Interlude"
|
||||
],
|
||||
[
|
||||
"content/88243afbe5ed8.nwd",
|
||||
"NOVEL",
|
||||
"Scene One"
|
||||
],
|
||||
[
|
||||
"content/88d59a277361b.nwd",
|
||||
"NOVEL",
|
||||
"Prologue"
|
||||
],
|
||||
[
|
||||
"content/8c58a65414c23.nwd",
|
||||
"NOVEL",
|
||||
"Front Matter"
|
||||
],
|
||||
[
|
||||
"content/db7e733775d4d.nwd",
|
||||
"NOVEL",
|
||||
"Act One"
|
||||
],
|
||||
[
|
||||
"content/eb103bc70c90c.nwd",
|
||||
"NOVEL",
|
||||
"Scene Three"
|
||||
],
|
||||
[
|
||||
"content/f8c0562e50f1b.nwd",
|
||||
"NOVEL",
|
||||
"Scene Four"
|
||||
],
|
||||
[
|
||||
"content/f96ec11c6a3da.nwd",
|
||||
"NOVEL",
|
||||
"Scene Two"
|
||||
],
|
||||
[
|
||||
"content/fb609cd8319dc.nwd",
|
||||
"NOVEL",
|
||||
"Chapter One"
|
||||
]
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
[
|
||||
[
|
||||
"content/8c659a11cd429.nwd",
|
||||
"NOVEL",
|
||||
"New Scene"
|
||||
],
|
||||
[
|
||||
"content/a35baf2e93843.nwd",
|
||||
"NOVEL",
|
||||
"Title Page"
|
||||
],
|
||||
[
|
||||
"content/f5ab3e30151e1.nwd",
|
||||
"NOVEL",
|
||||
"New Chapter"
|
||||
]
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Test Dummy GUI Classes
|
||||
"""
|
||||
|
||||
class DummyMain():
|
||||
|
||||
def __init__(self):
|
||||
self.mainConf = None
|
||||
self.hasProject = True
|
||||
self.statusBar = StatusBar()
|
||||
return
|
||||
|
||||
def makeAlert(self, theMessage, theLevel):
|
||||
print("%s: %s" % (str(theLevel), theMessage))
|
||||
return
|
||||
|
||||
def setStatus(self, theMessage):
|
||||
return
|
||||
|
||||
def setProjectStatus(self, isChanged):
|
||||
return
|
||||
|
||||
def openProject(self, projPath):
|
||||
return
|
||||
|
||||
def rebuildIndex(self):
|
||||
return
|
||||
|
||||
# END Class GuiMain
|
||||
|
||||
class StatusBar():
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def setStatus(self, theText):
|
||||
return
|
||||
|
||||
# END Class StatusBar
|
||||
@@ -6,6 +6,7 @@ icons = typicons_colour_light
|
||||
guidark = False
|
||||
guifont =
|
||||
guifontsize = 11
|
||||
lastnotes = 1.0
|
||||
|
||||
[Sizes]
|
||||
geometry = 1200, 650
|
||||
@@ -41,56 +41,56 @@
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="23">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="None" parent="None">
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>Locations</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="None">
|
||||
<item handle="25fc0e7096fc6" order="0" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="None">
|
||||
<item handle="31489056e0916" order="0" parent="None">
|
||||
<name>Objects</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="None">
|
||||
<item handle="98010bd9270f9" order="0" parent="None">
|
||||
<name>Entity</name>
|
||||
<type>ROOT</type>
|
||||
<class>ENTITY</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
|
||||
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -102,14 +102,14 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
|
||||
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
|
||||
<name>Chapter 1</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="1a6562590ef19">
|
||||
<item handle="031b4af5197ec" order="0" parent="1a6562590ef19">
|
||||
<name>Chapter 1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -121,7 +121,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="1a6562590ef19">
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="1a6562590ef19">
|
||||
<name>Scene 1.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -133,7 +133,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="None" parent="1a6562590ef19">
|
||||
<item handle="2858dcd1057d3" order="0" parent="1a6562590ef19">
|
||||
<name>Scene 1.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -145,7 +145,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="None" parent="1a6562590ef19">
|
||||
<item handle="2fca346db6561" order="0" parent="1a6562590ef19">
|
||||
<name>Scene 1.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -157,14 +157,14 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
|
||||
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
|
||||
<name>Chapter 2</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="7688b6ef52555" order="None" parent="02d20bbd7e394">
|
||||
<item handle="7688b6ef52555" order="0" parent="02d20bbd7e394">
|
||||
<name>Chapter 2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -176,7 +176,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="c837649cce43f" order="None" parent="02d20bbd7e394">
|
||||
<item handle="c837649cce43f" order="0" parent="02d20bbd7e394">
|
||||
<name>Scene 2.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -188,7 +188,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="6208ef0f7750c" order="None" parent="02d20bbd7e394">
|
||||
<item handle="6208ef0f7750c" order="0" parent="02d20bbd7e394">
|
||||
<name>Scene 2.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -200,7 +200,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="3e1e967e9b793" order="None" parent="02d20bbd7e394">
|
||||
<item handle="3e1e967e9b793" order="0" parent="02d20bbd7e394">
|
||||
<name>Scene 2.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -212,14 +212,14 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="39fa9ec190eee" order="None" parent="73475cb40a568">
|
||||
<item handle="39fa9ec190eee" order="0" parent="73475cb40a568">
|
||||
<name>Chapter 3</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="d029fa3a95e17" order="None" parent="39fa9ec190eee">
|
||||
<item handle="d029fa3a95e17" order="0" parent="39fa9ec190eee">
|
||||
<name>Chapter 3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -231,7 +231,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="81b8a03f97e87" order="None" parent="39fa9ec190eee">
|
||||
<item handle="81b8a03f97e87" order="0" parent="39fa9ec190eee">
|
||||
<name>Scene 3.1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -243,7 +243,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="da4ea2a5506f2" order="None" parent="39fa9ec190eee">
|
||||
<item handle="da4ea2a5506f2" order="0" parent="39fa9ec190eee">
|
||||
<name>Scene 3.2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -255,7 +255,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="a68b412c42825" order="None" parent="39fa9ec190eee">
|
||||
<item handle="a68b412c42825" order="0" parent="39fa9ec190eee">
|
||||
<name>Scene 3.3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -41,56 +41,56 @@
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="14">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="None" parent="None">
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>Locations</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="None">
|
||||
<item handle="25fc0e7096fc6" order="0" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="None">
|
||||
<item handle="31489056e0916" order="0" parent="None">
|
||||
<name>Objects</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="None">
|
||||
<item handle="98010bd9270f9" order="0" parent="None">
|
||||
<name>Entity</name>
|
||||
<type>ROOT</type>
|
||||
<class>ENTITY</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
|
||||
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -102,7 +102,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
|
||||
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
|
||||
<name>Scene 1</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -114,7 +114,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="73475cb40a568">
|
||||
<item handle="031b4af5197ec" order="0" parent="73475cb40a568">
|
||||
<name>Scene 2</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -126,7 +126,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="73475cb40a568">
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="73475cb40a568">
|
||||
<name>Scene 3</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -138,7 +138,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="None" parent="73475cb40a568">
|
||||
<item handle="2858dcd1057d3" order="0" parent="73475cb40a568">
|
||||
<name>Scene 4</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -150,7 +150,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="None" parent="73475cb40a568">
|
||||
<item handle="2fca346db6561" order="0" parent="73475cb40a568">
|
||||
<name>Scene 5</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -162,7 +162,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
|
||||
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
|
||||
<name>Scene 6</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -39,35 +39,35 @@
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="10">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="None" parent="None">
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -79,14 +79,14 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
||||
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -98,7 +98,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
||||
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -110,7 +110,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="31489056e0916">
|
||||
<item handle="1a6562590ef19" order="0" parent="31489056e0916">
|
||||
<name>Hello</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -122,7 +122,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="71ee45a3c0db9">
|
||||
<item handle="031b4af5197ec" order="0" parent="71ee45a3c0db9">
|
||||
<name>Jane</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
@@ -39,35 +39,35 @@
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="8">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="None" parent="None">
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -79,14 +79,14 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
||||
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -98,7 +98,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
||||
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -39,35 +39,35 @@
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="12">
|
||||
<item handle="73475cb40a568" order="None" parent="None">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="None" parent="None">
|
||||
<item handle="44cb730c42048" order="0" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="None" parent="None">
|
||||
<item handle="71ee45a3c0db9" order="0" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="None" parent="None">
|
||||
<item handle="811786ad1ae74" order="0" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
|
||||
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
|
||||
<name>Title Page</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -79,14 +79,14 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="31489056e0916" order="None" parent="73475cb40a568">
|
||||
<item handle="31489056e0916" order="0" parent="73475cb40a568">
|
||||
<name>New Chapter</name>
|
||||
<type>FOLDER</type>
|
||||
<class>NOVEL</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
|
||||
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
|
||||
<name>New Chapter</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -98,7 +98,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
|
||||
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
|
||||
<name>New Scene</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -110,28 +110,28 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="1a6562590ef19" order="None" parent="None">
|
||||
<item handle="1a6562590ef19" order="0" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="031b4af5197ec" order="None" parent="None">
|
||||
<item handle="031b4af5197ec" order="0" parent="None">
|
||||
<name>Object</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="41cfc0d1f2d12" order="None" parent="None">
|
||||
<item handle="41cfc0d1f2d12" order="0" parent="None">
|
||||
<name>Custom1</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="2858dcd1057d3" order="None" parent="None">
|
||||
<item handle="2858dcd1057d3" order="0" parent="None">
|
||||
<name>Custom2</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
@@ -6,6 +6,7 @@ icons = typicons_colour_light
|
||||
guidark = True
|
||||
guifont = Cantarell
|
||||
guifontsize = 12
|
||||
lastnotes = 1.0
|
||||
|
||||
[Sizes]
|
||||
geometry = 1100, 650
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Common Class Tester
|
||||
"""novelWriter Common Functions Tester
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -9,10 +9,12 @@ from nw.common import (
|
||||
checkString, checkBool, checkInt, colRange, formatInt, transferCase,
|
||||
fuzzyTime, checkHandle, formatTimeStamp, formatTime
|
||||
)
|
||||
from nwtools import cmpList
|
||||
from tools import cmpList
|
||||
|
||||
@pytest.mark.core
|
||||
def testCheckString():
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_CheckString():
|
||||
"""Test the checkString function.
|
||||
"""
|
||||
assert checkString(None, "NotNone", True) is None
|
||||
assert checkString("None", "NotNone", True) is None
|
||||
assert checkString("None", "NotNone", False) == "None"
|
||||
@@ -21,8 +23,12 @@ def testCheckString():
|
||||
assert checkString(1.0, "NotNone", False) == "NotNone"
|
||||
assert checkString(True, "NotNone", False) == "NotNone"
|
||||
|
||||
@pytest.mark.core
|
||||
def testCheckInt():
|
||||
# END Test testBaseCommon_CheckString
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_CheckInt():
|
||||
"""Test the checkInt function.
|
||||
"""
|
||||
assert checkInt(None, 3, True) is None
|
||||
assert checkInt("None", 3, True) is None
|
||||
assert checkInt(None, 3, False) == 3
|
||||
@@ -30,8 +36,12 @@ def testCheckInt():
|
||||
assert checkInt(1.0, 3, False) == 1
|
||||
assert checkInt(True, 3, False) == 1
|
||||
|
||||
@pytest.mark.core
|
||||
def testCheckBool():
|
||||
# END Test testBaseCommon_CheckInt
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_CheckBool():
|
||||
"""Test the checkBool function.
|
||||
"""
|
||||
assert checkBool(None, 3, True) is None
|
||||
assert checkBool("None", 3, True) is None
|
||||
assert checkBool("True", False, False)
|
||||
@@ -44,8 +54,12 @@ def testCheckBool():
|
||||
assert checkBool(1.0, None, False) is None
|
||||
assert checkBool(2.0, None, False) is None
|
||||
|
||||
@pytest.mark.core
|
||||
def testCheckHandle():
|
||||
# END Test testBaseCommon_CheckBool
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_CheckHandle():
|
||||
"""Test the checkHandle function.
|
||||
"""
|
||||
assert checkHandle("None", 1, True) is None
|
||||
assert checkHandle("None", 1, False) == 1
|
||||
assert checkHandle(None, 1, True) is None
|
||||
@@ -53,8 +67,12 @@ def testCheckHandle():
|
||||
assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf"
|
||||
assert checkHandle("h7666c91c7ccf", None, False) is None
|
||||
|
||||
@pytest.mark.core
|
||||
def testColRange():
|
||||
# END Test testBaseCommon_CheckHandle
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_ColRange():
|
||||
"""Test the colRange function.
|
||||
"""
|
||||
assert colRange([0, 0], [0, 0], 0) is None
|
||||
assert cmpList(
|
||||
colRange([200, 50, 0], [50, 200, 0], 1),
|
||||
@@ -77,14 +95,22 @@ def testColRange():
|
||||
[[200, 50, 0], [162, 87, 0], [124, 124, 0], [86, 161, 0], [50, 200, 0]]
|
||||
)
|
||||
|
||||
@pytest.mark.core
|
||||
def testFormatTimeStamp():
|
||||
# END Test testBaseCommon_ColRange
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_FormatTimeStamp():
|
||||
"""Test the formatTimeStamp function.
|
||||
"""
|
||||
tTime = time.mktime(time.gmtime(0))
|
||||
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
|
||||
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
|
||||
|
||||
@pytest.mark.core
|
||||
def testFormatTime():
|
||||
# END Test testBaseCommon_FormatTimeStamp
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_FormatTime():
|
||||
"""Test the formatTime function.
|
||||
"""
|
||||
assert formatTime("1") == "ERROR"
|
||||
assert formatTime(1.0) == "ERROR"
|
||||
assert formatTime(1) == "00:00:01"
|
||||
@@ -101,8 +127,12 @@ def testFormatTime():
|
||||
assert formatTime(86400) == "1-00:00:00"
|
||||
assert formatTime(360000) == "4-04:00:00"
|
||||
|
||||
@pytest.mark.core
|
||||
def testFormatInt():
|
||||
# END Test testBaseCommon_FormatTime
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_FormatInt():
|
||||
"""Test the formatInt function.
|
||||
"""
|
||||
assert formatInt(1000) == "1000"
|
||||
assert formatInt(1234) == "1.23\u2009k"
|
||||
assert formatInt(12345) == "12.3\u2009k"
|
||||
@@ -112,8 +142,12 @@ def testFormatInt():
|
||||
assert formatInt(123456789) == "123\u2009M"
|
||||
assert formatInt(1234567890) == "1.23\u2009G"
|
||||
|
||||
@pytest.mark.core
|
||||
def testTransferCase():
|
||||
# END Test testBaseCommon_FormatInt
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_TransferCase():
|
||||
"""Test the transferCase function.
|
||||
"""
|
||||
assert transferCase(1, "TaRgEt") == "TaRgEt"
|
||||
assert transferCase("source", 1) == 1
|
||||
assert transferCase("", "TaRgEt") == "TaRgEt"
|
||||
@@ -122,8 +156,12 @@ def testTransferCase():
|
||||
assert transferCase("SOURCE", "target") == "TARGET"
|
||||
assert transferCase("source", "TARGET") == "target"
|
||||
|
||||
@pytest.mark.core
|
||||
def testFuzzyTime():
|
||||
# END Test testBaseCommon_TransferCase
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_FuzzyTime():
|
||||
"""Test the fuzzyTime function.
|
||||
"""
|
||||
assert fuzzyTime(-1) == "in the future"
|
||||
assert fuzzyTime(0) == "just now"
|
||||
assert fuzzyTime(29) == "just now"
|
||||
@@ -152,3 +190,5 @@ def testFuzzyTime():
|
||||
assert fuzzyTime(29808000) == "a year ago"
|
||||
assert fuzzyTime(47336399) == "a year ago"
|
||||
assert fuzzyTime(47336400) == "2 years ago"
|
||||
|
||||
# END Test testBaseCommon_FuzzyTime
|
||||
@@ -0,0 +1,505 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Config Class Tester
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
import configparser
|
||||
|
||||
from shutil import copyfile
|
||||
|
||||
from dummy import causeOSError
|
||||
from tools import cmpFiles
|
||||
|
||||
from nw.config import Config
|
||||
from nw.constants import nwConst, nwFiles
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseConfig_Constructor(monkeypatch):
|
||||
"""Test config contructor.
|
||||
"""
|
||||
# Linux
|
||||
monkeypatch.setattr("sys.platform", "linux")
|
||||
tstConf = Config()
|
||||
assert tstConf.osLinux is True
|
||||
assert tstConf.osDarwin is False
|
||||
assert tstConf.osWindows is False
|
||||
assert tstConf.osUnknown is False
|
||||
monkeypatch.undo()
|
||||
|
||||
# macOS
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
tstConf = Config()
|
||||
assert tstConf.osLinux is False
|
||||
assert tstConf.osDarwin is True
|
||||
assert tstConf.osWindows is False
|
||||
assert tstConf.osUnknown is False
|
||||
monkeypatch.undo()
|
||||
|
||||
# Windows
|
||||
monkeypatch.setattr("sys.platform", "win32")
|
||||
tstConf = Config()
|
||||
assert tstConf.osLinux is False
|
||||
assert tstConf.osDarwin is False
|
||||
assert tstConf.osWindows is True
|
||||
assert tstConf.osUnknown is False
|
||||
monkeypatch.undo()
|
||||
|
||||
# Cygwin
|
||||
monkeypatch.setattr("sys.platform", "cygwin")
|
||||
tstConf = Config()
|
||||
assert tstConf.osLinux is False
|
||||
assert tstConf.osDarwin is False
|
||||
assert tstConf.osWindows is True
|
||||
assert tstConf.osUnknown is False
|
||||
monkeypatch.undo()
|
||||
|
||||
# Other
|
||||
monkeypatch.setattr("sys.platform", "some_ther_os")
|
||||
tstConf = Config()
|
||||
assert tstConf.osLinux is False
|
||||
assert tstConf.osDarwin is False
|
||||
assert tstConf.osWindows is False
|
||||
assert tstConf.osUnknown is True
|
||||
monkeypatch.undo()
|
||||
|
||||
# END Test testBaseConfig_Constructor
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir):
|
||||
"""Test config intialisation.
|
||||
"""
|
||||
tstConf = Config()
|
||||
|
||||
confFile = os.path.join(tmpDir, "novelwriter.conf")
|
||||
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
|
||||
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")
|
||||
|
||||
# Make sure we don't have any old conf file
|
||||
if os.path.isfile(confFile):
|
||||
os.unlink(confFile)
|
||||
|
||||
# Let the config class figure out the path
|
||||
monkeypatch.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *args: fncDir)
|
||||
tstConf.verQtValue = 50600
|
||||
tstConf.initConfig()
|
||||
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
|
||||
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
|
||||
assert not os.path.isfile(confFile)
|
||||
tstConf.verQtValue = 50000
|
||||
tstConf.initConfig()
|
||||
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
|
||||
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
|
||||
assert not os.path.isfile(confFile)
|
||||
monkeypatch.undo()
|
||||
|
||||
# Fail to make folders
|
||||
monkeypatch.setattr("os.mkdir", causeOSError)
|
||||
|
||||
tstConfDir = os.path.join(fncDir, "test_conf")
|
||||
tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir)
|
||||
assert tstConf.confPath is None
|
||||
assert tstConf.dataPath == tmpDir
|
||||
assert not os.path.isfile(confFile)
|
||||
|
||||
tstDataDir = os.path.join(fncDir, "test_data")
|
||||
tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir)
|
||||
assert tstConf.confPath == tmpDir
|
||||
assert tstConf.dataPath is None
|
||||
assert os.path.isfile(confFile)
|
||||
os.unlink(confFile)
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
# Test load/save with no path
|
||||
tstConf.confPath = None
|
||||
assert not tstConf.loadConfig()
|
||||
assert not tstConf.saveConfig()
|
||||
|
||||
# Run again and set the paths directly and correctly
|
||||
# This should create a config file as well
|
||||
monkeypatch.setattr("os.path.expanduser", lambda *args: "")
|
||||
tstConf.spellTool = nwConst.SP_INTERNAL
|
||||
tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir)
|
||||
assert tstConf.confPath == tmpDir
|
||||
assert tstConf.dataPath == tmpDir
|
||||
assert os.path.isfile(confFile)
|
||||
|
||||
copyfile(confFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 9])
|
||||
monkeypatch.undo()
|
||||
|
||||
# Load and save with OSError
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
|
||||
assert not tstConf.loadConfig()
|
||||
assert tstConf.hasError is True
|
||||
assert tstConf.errData != []
|
||||
assert tstConf.getErrData().startswith("Could not")
|
||||
assert tstConf.hasError is False
|
||||
assert tstConf.errData == []
|
||||
|
||||
assert not tstConf.saveConfig()
|
||||
assert tstConf.hasError is True
|
||||
assert tstConf.errData != []
|
||||
assert tstConf.getErrData().startswith("Could not")
|
||||
assert tstConf.hasError is False
|
||||
assert tstConf.errData == []
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
assert tstConf.loadConfig()
|
||||
assert tstConf.saveConfig()
|
||||
|
||||
copyfile(confFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 9])
|
||||
|
||||
# END Test testBaseConfig_Init
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir):
|
||||
"""Test recent cache file.
|
||||
"""
|
||||
# Check failing
|
||||
tmpConf.dataPath = None
|
||||
assert not tmpConf.loadRecentCache()
|
||||
assert not tmpConf.saveRecentCache()
|
||||
tmpConf.dataPath = tmpDir
|
||||
|
||||
# Add a couple of values
|
||||
pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE)
|
||||
pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE)
|
||||
assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000)
|
||||
assert tmpConf.updateRecentCache(pathTwo, "Proj Two", 200, 1600005600)
|
||||
assert tmpConf.recentProj == {
|
||||
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
|
||||
}
|
||||
|
||||
# Fail to Save
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
assert not tmpConf.saveRecentCache()
|
||||
monkeypatch.undo()
|
||||
|
||||
# Save Proper
|
||||
cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE)
|
||||
assert tmpConf.saveRecentCache()
|
||||
assert tmpConf.saveRecentCache()
|
||||
assert os.path.isfile(cacheFile)
|
||||
|
||||
# Fail to Load
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
tmpConf.recentProj = {}
|
||||
assert not tmpConf.loadRecentCache()
|
||||
assert tmpConf.recentProj == {}
|
||||
monkeypatch.undo()
|
||||
|
||||
# Load Proper
|
||||
tmpConf.recentProj = {}
|
||||
assert tmpConf.loadRecentCache()
|
||||
assert tmpConf.recentProj == {
|
||||
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
|
||||
}
|
||||
|
||||
# Remove Non-Existent Entry
|
||||
assert not tmpConf.removeFromRecentCache("dummy")
|
||||
assert tmpConf.recentProj == {
|
||||
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
|
||||
}
|
||||
|
||||
# Remove Second Entry
|
||||
assert tmpConf.removeFromRecentCache(pathTwo)
|
||||
assert tmpConf.recentProj == {
|
||||
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
|
||||
}
|
||||
|
||||
# END Test testBaseConfig_RecentCache
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseConfig_SetPath(tmpConf, tmpDir):
|
||||
"""Test path setters.
|
||||
"""
|
||||
# Conf Path
|
||||
assert tmpConf.setConfPath(None)
|
||||
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
|
||||
assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf"))
|
||||
assert tmpConf.confPath == tmpDir
|
||||
assert tmpConf.confFile == "novelwriter.conf"
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
# Data Path
|
||||
assert tmpConf.setDataPath(None)
|
||||
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
|
||||
assert tmpConf.setDataPath(tmpDir)
|
||||
assert tmpConf.dataPath == tmpDir
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
# Last Path
|
||||
assert tmpConf.setLastPath(None)
|
||||
assert tmpConf.lastPath == ""
|
||||
|
||||
assert tmpConf.setLastPath(os.path.join(tmpDir, "file.tmp"))
|
||||
assert tmpConf.lastPath == tmpDir
|
||||
|
||||
assert tmpConf.setLastPath("")
|
||||
assert tmpConf.lastPath == ""
|
||||
|
||||
# END Test testBaseConfig_SetPath
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
|
||||
"""Set various sizes and positions
|
||||
"""
|
||||
confFile = os.path.join(tmpDir, "novelwriter.conf")
|
||||
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
|
||||
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")
|
||||
|
||||
# GUI Scaling
|
||||
# ===========
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.pxInt(10) == 10
|
||||
assert tmpConf.pxInt(13) == 13
|
||||
assert tmpConf.rpxInt(10) == 10
|
||||
assert tmpConf.rpxInt(13) == 13
|
||||
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.pxInt(10) == 20
|
||||
assert tmpConf.pxInt(13) == 26
|
||||
assert tmpConf.rpxInt(10) == 5
|
||||
assert tmpConf.rpxInt(13) == 6
|
||||
|
||||
# Setter + Getter Combos
|
||||
# ======================
|
||||
|
||||
# Window Size
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setWinSize(1205, 655)
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setWinSize(70, 70)
|
||||
assert tmpConf.getWinSize() == [70, 70]
|
||||
assert tmpConf.winGeometry == [35, 35]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setWinSize(70, 70)
|
||||
assert tmpConf.getWinSize() == [70, 70]
|
||||
assert tmpConf.winGeometry == [70, 70]
|
||||
|
||||
assert tmpConf.setWinSize(1200, 650)
|
||||
|
||||
# Project Tree Columns
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||
assert tmpConf.getTreeColWidths() == [10, 20, 24]
|
||||
assert tmpConf.treeColWidth == [5, 10, 12]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||
assert tmpConf.getTreeColWidths() == [10, 20, 25]
|
||||
assert tmpConf.treeColWidth == [10, 20, 25]
|
||||
|
||||
assert tmpConf.setTreeColWidths([200, 50, 30])
|
||||
|
||||
# Project Settings Tree Columns
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||
assert tmpConf.getProjColWidths() == [10, 20, 30]
|
||||
assert tmpConf.projColWidth == [5, 10, 15]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||
assert tmpConf.getProjColWidths() == [10, 20, 30]
|
||||
assert tmpConf.projColWidth == [10, 20, 30]
|
||||
|
||||
assert tmpConf.setProjColWidths([200, 60, 140])
|
||||
|
||||
# Main Pane Splitter
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setMainPanePos([200, 700])
|
||||
assert tmpConf.getMainPanePos() == [200, 700]
|
||||
assert tmpConf.mainPanePos == [100, 350]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setMainPanePos([200, 700])
|
||||
assert tmpConf.getMainPanePos() == [200, 700]
|
||||
assert tmpConf.mainPanePos == [200, 700]
|
||||
|
||||
assert tmpConf.setMainPanePos([300, 800])
|
||||
|
||||
# Doc Pane Splitter
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setDocPanePos([300, 300])
|
||||
assert tmpConf.getDocPanePos() == [300, 300]
|
||||
assert tmpConf.docPanePos == [150, 150]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setDocPanePos([300, 300])
|
||||
assert tmpConf.getDocPanePos() == [300, 300]
|
||||
assert tmpConf.docPanePos == [300, 300]
|
||||
|
||||
assert tmpConf.setDocPanePos([400, 400])
|
||||
|
||||
# View Pane Splitter
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setViewPanePos([400, 250])
|
||||
assert tmpConf.getViewPanePos() == [400, 250]
|
||||
assert tmpConf.viewPanePos == [200, 125]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setViewPanePos([400, 250])
|
||||
assert tmpConf.getViewPanePos() == [400, 250]
|
||||
assert tmpConf.viewPanePos == [400, 250]
|
||||
|
||||
assert tmpConf.setViewPanePos([500, 150])
|
||||
|
||||
# Outline Pane Splitter
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setOutlinePanePos([400, 250])
|
||||
assert tmpConf.getOutlinePanePos() == [400, 250]
|
||||
assert tmpConf.outlnPanePos == [200, 125]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setOutlinePanePos([400, 250])
|
||||
assert tmpConf.getOutlinePanePos() == [400, 250]
|
||||
assert tmpConf.outlnPanePos == [400, 250]
|
||||
|
||||
assert tmpConf.setOutlinePanePos([500, 150])
|
||||
|
||||
# Getters Only
|
||||
# ============
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.getTextWidth() == 600
|
||||
assert tmpConf.getTextMargin() == 40
|
||||
assert tmpConf.getTabWidth() == 40
|
||||
assert tmpConf.getFocusWidth() == 800
|
||||
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.getTextWidth() == 1200
|
||||
assert tmpConf.getTextMargin() == 80
|
||||
assert tmpConf.getTabWidth() == 80
|
||||
assert tmpConf.getFocusWidth() == 1600
|
||||
|
||||
# Flag Setters
|
||||
# ============
|
||||
assert not tmpConf.setShowRefPanel(False)
|
||||
assert not tmpConf.showRefPanel
|
||||
assert tmpConf.setShowRefPanel(True)
|
||||
|
||||
assert not tmpConf.setViewComments(False)
|
||||
assert not tmpConf.viewComments
|
||||
assert tmpConf.setViewComments(True)
|
||||
|
||||
assert not tmpConf.setViewSynopsis(False)
|
||||
assert not tmpConf.viewSynopsis
|
||||
assert tmpConf.setViewSynopsis(True)
|
||||
|
||||
# Check Final File
|
||||
# ================
|
||||
|
||||
assert tmpConf.confChanged
|
||||
assert tmpConf.saveConfig()
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
copyfile(confFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, [2, 9])
|
||||
|
||||
# END Test testBaseConfig_SettersGetters
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseConfig_Internal(monkeypatch, tmpConf):
|
||||
"""Check internal functions.
|
||||
"""
|
||||
# Function _packList
|
||||
assert tmpConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False"
|
||||
|
||||
# Function _unpackList
|
||||
assert tmpConf._unpackList("1, 2, 3", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 3]
|
||||
assert tmpConf._unpackList("1, 2 ", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 0]
|
||||
assert tmpConf._unpackList("A, B, C", [0, 0, 0], tmpConf.CNF_I_LST) == [0, 0, 0]
|
||||
assert tmpConf._unpackList("1, 2, 3", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["1", "2", "3"]
|
||||
assert tmpConf._unpackList("A, B ", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "Z"]
|
||||
assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "C"]
|
||||
assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_STR) == ["X", "Y", "Z"]
|
||||
|
||||
# Function _parseLine
|
||||
cnfParse = configparser.ConfigParser()
|
||||
cnfParse.read_string(
|
||||
"[Main]\n"
|
||||
"val_string = dummy\n"
|
||||
"val_int = 123\n"
|
||||
"val_bool = True\n"
|
||||
"val_list_string = A, B, C\n"
|
||||
"val_list_int = 1, 2, 3\n"
|
||||
)
|
||||
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_string", tmpConf.CNF_STR, "default"
|
||||
) == "dummy"
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "nope", tmpConf.CNF_STR, "default"
|
||||
) == "default"
|
||||
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_int", tmpConf.CNF_INT, "0"
|
||||
) == 123
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "nope", tmpConf.CNF_INT, 0
|
||||
) == 0
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_string", tmpConf.CNF_INT, 0
|
||||
) == 0
|
||||
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_bool", tmpConf.CNF_BOOL, False
|
||||
) is True
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "nope", tmpConf.CNF_BOOL, False
|
||||
) is False
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_string", tmpConf.CNF_BOOL, False
|
||||
) is False
|
||||
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_list_string", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
|
||||
) == ["A", "B", "C", "Z"]
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "nope", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
|
||||
) == ["W", "X", "Y", "Z"]
|
||||
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "val_list_int", tmpConf.CNF_I_LST, [6, 7, 8, 9]
|
||||
) == [1, 2, 3, 9]
|
||||
assert tmpConf._parseLine(
|
||||
cnfParse, "Main", "nope", tmpConf.CNF_S_LST, [6, 7, 8, 9]
|
||||
) == [6, 7, 8, 9]
|
||||
|
||||
# Function _checkNone
|
||||
assert tmpConf._checkNone(None) is None
|
||||
assert tmpConf._checkNone("None") is None
|
||||
assert tmpConf._checkNone("stuff") == "stuff"
|
||||
|
||||
# Function _checkOptionalPackages
|
||||
# (Assumes enchant package exists ans is importable)
|
||||
tmpConf._checkOptionalPackages()
|
||||
assert tmpConf.hasEnchant is True
|
||||
|
||||
monkeypatch.setitem(sys.modules, "enchant", None)
|
||||
tmpConf._checkOptionalPackages()
|
||||
assert tmpConf.hasEnchant is False
|
||||
monkeypatch.undo()
|
||||
|
||||
monkeypatch.setattr("shutil.which", lambda *args: "dummy")
|
||||
tmpConf._checkOptionalPackages()
|
||||
assert tmpConf.hasAssistant is True
|
||||
monkeypatch.undo()
|
||||
|
||||
monkeypatch.setattr("shutil.which", lambda *args: None)
|
||||
tmpConf._checkOptionalPackages()
|
||||
assert tmpConf.hasAssistant is False
|
||||
monkeypatch.undo()
|
||||
|
||||
# END Test testBaseConfig_Internal
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Error Tester
|
||||
"""
|
||||
|
||||
import nw
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
from dummy import causeException
|
||||
|
||||
from nw.error import NWErrorMessage, exceptionHandler
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir):
|
||||
"""Test the error dialog.
|
||||
"""
|
||||
qApp.closeAllWindows()
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
|
||||
nwErr = NWErrorMessage(nwGUI)
|
||||
qtbot.addWidget(nwErr)
|
||||
nwErr.show()
|
||||
|
||||
# Invalid Error Message
|
||||
nwErr.setMessage(Exception, "Faulty Error", 123)
|
||||
assert nwErr.msgBody.toPlainText() == "Failed to generate error report ..."
|
||||
|
||||
# Valid Error Message
|
||||
monkeypatch.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", lambda: "1.2.3")
|
||||
nwErr.setMessage(Exception, "Fine Error", None)
|
||||
theMessage = nwErr.msgBody.toPlainText()
|
||||
assert theMessage
|
||||
assert "Fine Error" in theMessage
|
||||
assert "Exception" in theMessage
|
||||
assert "(1.2.3)" in theMessage
|
||||
monkeypatch.undo()
|
||||
|
||||
# No kernel version retrieved
|
||||
monkeypatch.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", causeException)
|
||||
nwErr.setMessage(Exception, "Almost Fine Error", None)
|
||||
theMessage = nwErr.msgBody.toPlainText()
|
||||
assert theMessage
|
||||
assert "(Unknown)" in theMessage
|
||||
monkeypatch.undo()
|
||||
|
||||
nwErr._doClose()
|
||||
nwErr.close()
|
||||
nwGUI.closeMain()
|
||||
|
||||
# END Test testBaseError_Dialog
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseError_Handler(qtbot, monkeypatch, fncDir, tmpDir):
|
||||
"""Test the error handler. This test doesn'thave any asserts, but it
|
||||
checks that the error handler handles potential exceptions. The test
|
||||
will fail if excpetions are not handled.
|
||||
"""
|
||||
qApp.closeAllWindows()
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
|
||||
# Normal shutdown
|
||||
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||
exceptionHandler(Exception, "Error Message", None)
|
||||
monkeypatch.undo()
|
||||
|
||||
# Should not crash when no GUI is found
|
||||
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.qApp.topLevelWidgets", lambda: [])
|
||||
exceptionHandler(Exception, "Error Message", None)
|
||||
monkeypatch.undo()
|
||||
|
||||
# Should handle qApp failing
|
||||
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.qApp.topLevelWidgets", causeException)
|
||||
exceptionHandler(Exception, "Error Message", None)
|
||||
monkeypatch.undo()
|
||||
|
||||
# Should handle failing to close main GUI
|
||||
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
|
||||
monkeypatch.setattr(nwGUI, "closeMain", causeException)
|
||||
exceptionHandler(Exception, "Error Message", None)
|
||||
monkeypatch.undo()
|
||||
|
||||
nwGUI.closeMain()
|
||||
|
||||
# END Test testBaseError_Handler
|
||||
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Main Init Tester
|
||||
"""
|
||||
|
||||
import nw
|
||||
import pytest
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from dummy import DummyMain
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseInit_Launch(caplog, monkeypatch, tmpDir):
|
||||
"""Check launching the main GUI.
|
||||
"""
|
||||
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
|
||||
|
||||
# Testmode launch
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert isinstance(nwGUI, DummyMain)
|
||||
|
||||
# Darwin launch
|
||||
monkeypatch.setitem(sys.modules, "Foundation", None)
|
||||
osDarwin = nw.CONFIG.osDarwin
|
||||
nw.CONFIG.osDarwin = True
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert isinstance(nwGUI, DummyMain)
|
||||
assert "Foundation" in caplog.messages[1]
|
||||
nw.CONFIG.osDarwin = osDarwin
|
||||
|
||||
# Normal launch
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationName", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationVersion", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setWindowIcon", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *args: 0)
|
||||
with pytest.raises(SystemExit) as ex:
|
||||
nw.main(["--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
|
||||
assert ex.value.code == 0
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
# END Test testBaseInit_Launch
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseInit_Options(monkeypatch, tmpDir):
|
||||
"""Test command line options for logging level.
|
||||
"""
|
||||
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
|
||||
monkeypatch.setattr(sys, "argv", [
|
||||
"novelWriter.py", "--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir
|
||||
])
|
||||
|
||||
# Defaults w/None Args
|
||||
nwGUI = nw.main()
|
||||
assert nw.logger.getEffectiveLevel() == logging.WARNING
|
||||
assert nw.CONFIG.debugInfo is False
|
||||
assert nw.CONFIG.showGUI is False
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
|
||||
# Defaults
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "--style=Fusion"]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == logging.WARNING
|
||||
assert nw.CONFIG.debugInfo is False
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
|
||||
# Log Levels
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--info", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == logging.INFO
|
||||
assert nw.CONFIG.debugInfo is False
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--debug", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == logging.DEBUG
|
||||
assert nw.CONFIG.debugInfo is True
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--verbose", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == 5
|
||||
assert nw.CONFIG.debugInfo is True
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
|
||||
# Help and Version
|
||||
with pytest.raises(SystemExit) as ex:
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--help", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
assert ex.value.code == 0
|
||||
|
||||
with pytest.raises(SystemExit) as ex:
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--version", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
assert ex.value.code == 0
|
||||
|
||||
# Invalid options
|
||||
with pytest.raises(SystemExit) as ex:
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--invalid", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
assert ex.value.code == 2
|
||||
|
||||
# Project Path
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "sample/"]
|
||||
)
|
||||
assert nw.CONFIG.cmdOpen == "sample/"
|
||||
assert nwGUI.closeMain() == "closeMain"
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
# END Test testBaseInit_Options
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseInit_Imports(caplog, monkeypatch, tmpDir):
|
||||
"""Check import error handling.
|
||||
"""
|
||||
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *args: 0)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.__init__", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.resize", lambda *args: None)
|
||||
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.showMessage", lambda *args: 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:
|
||||
_ = nw.main(
|
||||
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
|
||||
)
|
||||
|
||||
assert ex.value.code & 4 == 4 # Python version not satisfied
|
||||
assert ex.value.code & 8 == 8 # Qt version not satisfied
|
||||
assert ex.value.code & 16 == 16 # PyQt version not satisfied
|
||||
assert ex.value.code & 32 == 32 # lxml package missing
|
||||
|
||||
assert "At least Python" in caplog.messages[0]
|
||||
assert "At least Qt5" in caplog.messages[1]
|
||||
assert "At least PyQt5" in caplog.messages[2]
|
||||
assert "lxml" in caplog.messages[3]
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
# END Test testBaseInit_Imports
|
||||
@@ -1,165 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Config Class Tester
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from nwtools import cmpFiles
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigCore(tmpConf, nwTemp, nwRef):
|
||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
||||
|
||||
assert tmpConf.confPath == nwTemp
|
||||
assert tmpConf.saveConfig()
|
||||
assert cmpFiles(testConf, refConf, [2])
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
assert tmpConf.loadConfig()
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigSetConfPath(tmpConf, nwTemp):
|
||||
assert tmpConf.setConfPath(None)
|
||||
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
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigSetDataPath(tmpConf, nwTemp):
|
||||
assert tmpConf.setDataPath(None)
|
||||
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 = os.path.join(nwRef, "novelwriter.conf")
|
||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
||||
tmpConf.guiScale = 1.0
|
||||
|
||||
assert tmpConf.confPath == nwTemp
|
||||
assert tmpConf.setWinSize(1205, 655)
|
||||
assert not tmpConf.confChanged
|
||||
assert tmpConf.setWinSize(70, 70)
|
||||
assert tmpConf.confChanged
|
||||
assert tmpConf.setWinSize(1200, 650)
|
||||
assert tmpConf.saveConfig()
|
||||
|
||||
assert cmpFiles(testConf, refConf, [2])
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef):
|
||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
||||
|
||||
assert tmpConf.confPath == nwTemp
|
||||
tmpConf.guiScale = 1.0
|
||||
|
||||
assert tmpConf.setTreeColWidths([10, 20, 25])
|
||||
assert tmpConf.treeColWidth == [10, 20, 25]
|
||||
assert tmpConf.setTreeColWidths([200, 50, 30])
|
||||
|
||||
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||
assert tmpConf.projColWidth == [10, 20, 30]
|
||||
assert tmpConf.setProjColWidths([200, 60, 140])
|
||||
|
||||
assert tmpConf.confChanged
|
||||
assert tmpConf.saveConfig()
|
||||
|
||||
assert cmpFiles(testConf, refConf, [2])
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigSetPanePos(tmpConf, nwTemp, nwRef):
|
||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
||||
|
||||
assert tmpConf.confPath == nwTemp
|
||||
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setMainPanePos([200, 700])
|
||||
assert tmpConf.mainPanePos == [100, 350]
|
||||
assert tmpConf.getMainPanePos() == [200, 700]
|
||||
|
||||
assert tmpConf.setDocPanePos([300, 300])
|
||||
assert tmpConf.docPanePos == [150, 150]
|
||||
assert tmpConf.getDocPanePos() == [300, 300]
|
||||
|
||||
assert tmpConf.setViewPanePos([400, 250])
|
||||
assert tmpConf.viewPanePos == [200, 125]
|
||||
assert tmpConf.getViewPanePos() == [400, 250]
|
||||
|
||||
assert tmpConf.setOutlinePanePos([400, 250])
|
||||
assert tmpConf.outlnPanePos == [200, 125]
|
||||
assert tmpConf.getOutlinePanePos() == [400, 250]
|
||||
|
||||
tmpConf.guiScale = 1.0
|
||||
assert tmpConf.setMainPanePos([300, 800])
|
||||
assert tmpConf.setDocPanePos([400, 400])
|
||||
assert tmpConf.setViewPanePos([500, 150])
|
||||
assert tmpConf.setOutlinePanePos([500, 150])
|
||||
|
||||
assert tmpConf.confChanged
|
||||
assert tmpConf.saveConfig()
|
||||
|
||||
assert cmpFiles(testConf, refConf, [2])
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigFlags(tmpConf, nwTemp, nwRef):
|
||||
refConf = os.path.join(nwRef, "novelwriter.conf")
|
||||
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
|
||||
|
||||
assert tmpConf.confPath == nwTemp
|
||||
|
||||
assert not tmpConf.setShowRefPanel(False)
|
||||
assert tmpConf.setShowRefPanel(True)
|
||||
|
||||
assert not tmpConf.setViewComments(False)
|
||||
assert not tmpConf.viewComments
|
||||
assert tmpConf.setViewComments(True)
|
||||
|
||||
assert not tmpConf.setViewSynopsis(False)
|
||||
assert not tmpConf.viewSynopsis
|
||||
assert tmpConf.setViewSynopsis(True)
|
||||
|
||||
assert tmpConf.confChanged
|
||||
assert tmpConf.saveConfig()
|
||||
|
||||
assert cmpFiles(testConf, refConf, [2])
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testTextSizes(tmpConf, nwTemp, nwRef):
|
||||
assert tmpConf.confPath == nwTemp
|
||||
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.getTextWidth() == 1200
|
||||
assert tmpConf.getTextMargin() == 80
|
||||
assert tmpConf.getTabWidth() == 80
|
||||
assert tmpConf.getFocusWidth() == 1600
|
||||
tmpConf.guiScale = 1.0
|
||||
|
||||
assert not tmpConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigErrors(tmpConf):
|
||||
nonPath = os.path.join("somewhere", "over", "the", "rainbow")
|
||||
assert tmpConf.initConfig(nonPath, nonPath)
|
||||
assert tmpConf.hasError
|
||||
assert not tmpConf.loadConfig()
|
||||
assert not tmpConf.saveConfig()
|
||||
assert not tmpConf.loadRecentCache()
|
||||
assert len(tmpConf.getErrData()) > 0
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigInternals(tmpConf):
|
||||
assert tmpConf._checkNone(None) is None
|
||||
assert tmpConf._checkNone("None") is None
|
||||
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter NWDoc Class Tester
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from dummy import causeOSError
|
||||
|
||||
from nw.core import NWProject, NWDoc
|
||||
from nw.core.item import NWItem
|
||||
from nw.constants import nwItemClass, nwItemLayout
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal):
|
||||
"""Test loading and saving a document with the NWDoc class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
assert theProject.projPath == nwMinimal
|
||||
|
||||
theDoc = NWDoc(theProject, dummyGUI)
|
||||
sHandle = "8c659a11cd429"
|
||||
|
||||
# Not a valid handle
|
||||
assert theDoc.openDocument("dummy") is None
|
||||
|
||||
# Non-existent handle
|
||||
assert theDoc.openDocument("0000000000000") is None
|
||||
|
||||
# Cause open() to fail while loading
|
||||
def dummyOpen(*args, **kwargs):
|
||||
raise OSError
|
||||
|
||||
monkeypatch.setattr("builtins.open", dummyOpen)
|
||||
assert theDoc.openDocument(sHandle) is None
|
||||
monkeypatch.undo()
|
||||
|
||||
# Load the text
|
||||
assert theDoc.openDocument(sHandle) == "### New Scene\n\n"
|
||||
|
||||
# Try to open a new (non-existent) file
|
||||
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
|
||||
assert nHandle is not None
|
||||
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
|
||||
assert theDoc.openDocument(xHandle) == ""
|
||||
|
||||
# Check cached item
|
||||
assert isinstance(theDoc._theItem, NWItem)
|
||||
assert theDoc.openDocument(xHandle, isOrphan=True) == ""
|
||||
assert theDoc._theItem is None
|
||||
|
||||
# Set handle and save again
|
||||
theText = "### Test File\n\nText ...\n\n"
|
||||
assert theDoc.openDocument(xHandle) == ""
|
||||
assert theDoc.saveDocument(theText)
|
||||
|
||||
# Save again to ensure temp file and previous file is handled
|
||||
assert theDoc.saveDocument(theText)
|
||||
|
||||
# Check file content
|
||||
docPath = os.path.join(nwMinimal, "content", xHandle+".nwd")
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
assert inFile.read() == (
|
||||
"%%~name: New File\n"
|
||||
f"%%~path: a508bb932959c/{xHandle}\n"
|
||||
"%%~kind: NOVEL/SCENE\n"
|
||||
"### Test File\n\n"
|
||||
"Text ...\n\n"
|
||||
)
|
||||
|
||||
# Force no meta data
|
||||
theDoc._theItem = None
|
||||
assert theDoc.saveDocument(theText)
|
||||
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
assert inFile.read() == theText
|
||||
|
||||
# Cause open() to fail while saving
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
assert not theDoc.saveDocument(theText)
|
||||
monkeypatch.undo()
|
||||
|
||||
# Saving with no handle
|
||||
theDoc.clearDocument()
|
||||
assert not theDoc.saveDocument(theText)
|
||||
|
||||
# Delete the last document
|
||||
assert not theDoc.deleteDocument("dummy")
|
||||
assert os.path.isfile(docPath)
|
||||
|
||||
# Cause the delete to fail
|
||||
monkeypatch.setattr("os.unlink", causeOSError)
|
||||
assert not theDoc.deleteDocument(xHandle)
|
||||
monkeypatch.undo()
|
||||
|
||||
# Make the delete pass
|
||||
assert theDoc.deleteDocument(xHandle)
|
||||
assert not os.path.isfile(docPath)
|
||||
|
||||
# END Test testCoreDocument_Load
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal):
|
||||
"""Test other methods of the NWDoc class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
assert theProject.projPath == nwMinimal
|
||||
|
||||
theDoc = NWDoc(theProject, dummyGUI)
|
||||
sHandle = "8c659a11cd429"
|
||||
docPath = os.path.join(nwMinimal, "content", sHandle+".nwd")
|
||||
|
||||
assert theDoc.openDocument(sHandle) == "### New Scene\n\n"
|
||||
|
||||
# Check location
|
||||
assert theDoc.getFileLocation() == docPath
|
||||
|
||||
# Check the item
|
||||
assert theDoc.getCurrentItem() is not None
|
||||
assert theDoc.getCurrentItem().itemHandle == sHandle
|
||||
|
||||
# Check the meta
|
||||
theName, theParent, theClass, theLayout = theDoc.getMeta()
|
||||
assert theName == "New Scene"
|
||||
assert theParent == "a6d311a93600a"
|
||||
assert theClass == nwItemClass.NOVEL
|
||||
assert theLayout == nwItemLayout.SCENE
|
||||
|
||||
# Add meta data garbage
|
||||
assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n")
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
assert inFile.read() == (
|
||||
"%%~name: New Scene\n"
|
||||
f"%%~path: a6d311a93600a/{sHandle}\n"
|
||||
"%%~kind: NOVEL/SCENE\n"
|
||||
"%%~ stuff\n"
|
||||
"### Test File\n\n"
|
||||
"Text ...\n\n"
|
||||
)
|
||||
|
||||
assert theDoc.openDocument(sHandle) == "### Test File\n\nText ...\n\n"
|
||||
|
||||
# END Test testCoreDocument_Methods
|
||||
@@ -8,28 +8,28 @@ import json
|
||||
|
||||
from shutil import copyfile
|
||||
|
||||
from nwtools import cmpFiles
|
||||
from tools 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):
|
||||
@pytest.mark.core
|
||||
def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
|
||||
"""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")
|
||||
testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json")
|
||||
compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json")
|
||||
|
||||
theProject = NWProject(nwDummy)
|
||||
theProject = NWProject(dummyGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwLipsum)
|
||||
|
||||
monkeypatch.setattr("nw.core.index.time", lambda: 123.4)
|
||||
|
||||
theIndex = NWIndex(theProject, nwDummy)
|
||||
theIndex = NWIndex(theProject, dummyGUI)
|
||||
notIndexable = {
|
||||
"b3643d0f92e32": False, # Novel ROOT
|
||||
"45e6b01ca35c1": False, # Chapter One FOLDER
|
||||
@@ -136,17 +136,19 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef):
|
||||
assert theProject.closeProject()
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
assert cmpFiles(testFile, compFile)
|
||||
|
||||
@pytest.mark.project
|
||||
def testIndexScanThis(nwMinimal, nwDummy):
|
||||
# END Test testCoreIndex_LoadSave
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreIndex_ScanThis(nwMinimal, dummyGUI):
|
||||
"""Test the tag scanner function scanThis.
|
||||
"""
|
||||
theProject = NWProject(nwDummy)
|
||||
theProject = NWProject(dummyGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
|
||||
theIndex = NWIndex(theProject, nwDummy)
|
||||
theIndex = NWIndex(theProject, dummyGUI)
|
||||
|
||||
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
|
||||
assert not isValid
|
||||
@@ -162,40 +164,42 @@ def testIndexScanThis(nwMinimal, nwDummy):
|
||||
|
||||
isValid, theBits, thePos = theIndex.scanThis("@a:")
|
||||
assert isValid
|
||||
assert str(theBits) == "['@a']"
|
||||
assert str(thePos) == "[0]"
|
||||
assert theBits == ["@a"]
|
||||
assert thePos == [0]
|
||||
|
||||
isValid, theBits, thePos = theIndex.scanThis("@a:b")
|
||||
assert isValid
|
||||
assert str(theBits) == "['@a', 'b']"
|
||||
assert str(thePos) == "[0, 3]"
|
||||
assert theBits == ["@a", "b"]
|
||||
assert 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]"
|
||||
assert theBits == ["@a", "b", "c", "d"]
|
||||
assert 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]"
|
||||
assert theBits == ["@a", "b", "c", "d"]
|
||||
assert 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 theBits == ["@tag", "this", "and this"]
|
||||
assert thePos == [0, 6, 12]
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
@pytest.mark.project
|
||||
def testIndexCheckThese(nwMinimal, nwDummy):
|
||||
# END Test testCoreIndex_ScanThis
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
|
||||
"""Test the tag checker function checkThese.
|
||||
"""
|
||||
theProject = NWProject(nwDummy)
|
||||
theProject = NWProject(dummyGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
|
||||
theIndex = NWIndex(theProject, nwDummy)
|
||||
theIndex = NWIndex(theProject, dummyGUI)
|
||||
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
||||
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||
nItem = theProject.projTree[nHandle]
|
||||
@@ -209,29 +213,32 @@ def testIndexCheckThese(nwMinimal, nwDummy):
|
||||
"# Hello World!\n"
|
||||
"@pov: Jane"
|
||||
))
|
||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
|
||||
assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
|
||||
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 theIndex.checkThese([], cItem) == []
|
||||
assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True]
|
||||
assert theIndex.checkThese(["@tag", "John"], cItem) == [True, True]
|
||||
assert theIndex.checkThese(["@tag", "Jane"], nItem) == [True, False]
|
||||
assert theIndex.checkThese(["@tag", "John"], nItem) == [True, True]
|
||||
assert theIndex.checkThese(["@pov", "John"], nItem) == [True, False]
|
||||
assert theIndex.checkThese(["@pov", "Jane"], nItem) == [True, True]
|
||||
assert theIndex.checkThese(["@ pov", "Jane"], nItem) == [False, False]
|
||||
assert theIndex.checkThese(["@what", "Jane"], nItem) == [False, False]
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
@pytest.mark.project
|
||||
def testIndexScanText(nwMinimal, nwDummy):
|
||||
"""Check the index data extraction functions.
|
||||
# END Test testCoreIndex_CheckThese
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreIndex_ScanText(nwMinimal, dummyGUI):
|
||||
"""Check the index text scanner.
|
||||
"""
|
||||
theProject = NWProject(nwDummy)
|
||||
theProject = NWProject(dummyGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
|
||||
theIndex = NWIndex(theProject, nwDummy)
|
||||
theIndex = NWIndex(theProject, dummyGUI)
|
||||
|
||||
# Some items for fail to scan tests
|
||||
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
|
||||
@@ -392,21 +399,23 @@ def testIndexScanText(nwMinimal, nwDummy):
|
||||
"% synopsis: Synopsis One.\n\n"
|
||||
"Paragraph One.\n\n"
|
||||
))
|
||||
assert str(theIndex.refIndex[sHandle]["T000001"]["tags"]) == (
|
||||
"[[3, '@pov', 'One'], [5, '@char', 'Two']]"
|
||||
assert theIndex.refIndex[sHandle]["T000001"]["tags"] == (
|
||||
[[3, "@pov", "One"], [5, "@char", "Two"]]
|
||||
)
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
@pytest.mark.project
|
||||
def testIndexExtractData(nwMinimal, nwDummy):
|
||||
# END Test testCoreIndex_ScanText
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
|
||||
"""Check the index data extraction functions.
|
||||
"""
|
||||
theProject = NWProject(nwDummy)
|
||||
theProject = NWProject(dummyGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwMinimal)
|
||||
|
||||
theIndex = NWIndex(theProject, nwDummy)
|
||||
theIndex = NWIndex(theProject, dummyGUI)
|
||||
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
||||
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||
|
||||
@@ -424,7 +433,13 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
||||
))
|
||||
|
||||
# The novel structure should contain the pointer to the novel file header
|
||||
assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle
|
||||
assert theIndex.getNovelStructure() == ["%s:T000001" % nHandle]
|
||||
|
||||
# Check that excluded files can be skipped
|
||||
theProject.projTree[nHandle].setExported(False)
|
||||
assert theIndex.getNovelStructure(skipExcluded=False) == ["%s:T000001" % nHandle]
|
||||
assert theIndex.getNovelStructure(skipExcluded=True) == []
|
||||
assert theIndex.getNovelStructure() == []
|
||||
|
||||
# The novel file should have the correct counts
|
||||
cC, wC, pC = theIndex.getCounts(nHandle)
|
||||
@@ -443,8 +458,8 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
||||
|
||||
# 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']"
|
||||
assert theRefs["@pov"] == ["Jane"]
|
||||
assert theRefs["@char"] == ["Jane"]
|
||||
|
||||
##
|
||||
# getBackReferenceList
|
||||
@@ -455,7 +470,7 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
||||
|
||||
# The character file should have a record of the reference from the novel file
|
||||
theRefs = theIndex.getBackReferenceList(cHandle)
|
||||
assert str(theRefs) == "{'%s': 'T000001'}" % nHandle
|
||||
assert theRefs == {nHandle: "T000001"}
|
||||
|
||||
##
|
||||
# getTagSource
|
||||
@@ -535,3 +550,5 @@ def testIndexExtractData(nwMinimal, nwDummy):
|
||||
assert pC == 2
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
# END Test testCoreIndex_ExtractData
|
||||
@@ -6,13 +6,15 @@ import pytest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nw.core.project import NWProject, NWItem
|
||||
from nw.core import NWProject
|
||||
from nw.core.item import NWItem
|
||||
from nw.constants import nwItemClass, nwItemType, nwItemLayout
|
||||
|
||||
@pytest.mark.project
|
||||
def testItemSettersSimple(nwDummy):
|
||||
|
||||
theProject = NWProject(nwDummy)
|
||||
@pytest.mark.core
|
||||
def testCoreItem_Setters(dummyGUI):
|
||||
"""Test all the simple setters for the NWItem class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Name
|
||||
@@ -20,12 +22,16 @@ def testItemSettersSimple(nwDummy):
|
||||
assert theItem.itemName == "A Name"
|
||||
theItem.setName("\t A Name ")
|
||||
assert theItem.itemName == "A Name"
|
||||
theItem.setName(123)
|
||||
assert theItem.itemName == ""
|
||||
|
||||
# Handle
|
||||
theItem.setHandle(123)
|
||||
assert theItem.itemHandle is None
|
||||
theItem.setHandle("0123456789abcdef")
|
||||
assert theItem.itemHandle is None
|
||||
theItem.setHandle("0123456789abg")
|
||||
assert theItem.itemHandle is None
|
||||
theItem.setHandle("0123456789abc")
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
|
||||
@@ -36,6 +42,8 @@ def testItemSettersSimple(nwDummy):
|
||||
assert theItem.itemParent is None
|
||||
theItem.setParent("0123456789abcdef")
|
||||
assert theItem.itemParent is None
|
||||
theItem.setParent("0123456789abg")
|
||||
assert theItem.itemParent is None
|
||||
theItem.setParent("0123456789abc")
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
|
||||
@@ -59,6 +67,19 @@ def testItemSettersSimple(nwDummy):
|
||||
theItem.setStatus("Main")
|
||||
assert theItem.itemStatus == "Main"
|
||||
|
||||
# Importance
|
||||
theItem.itemClass = nwItemClass.NOVEL
|
||||
theItem.setStatus("Nonsense")
|
||||
assert theItem.itemStatus == "New"
|
||||
theItem.setStatus("New")
|
||||
assert theItem.itemStatus == "New"
|
||||
theItem.setStatus("Note")
|
||||
assert theItem.itemStatus == "Note"
|
||||
theItem.setStatus("Draft")
|
||||
assert theItem.itemStatus == "Draft"
|
||||
theItem.setStatus("Finished")
|
||||
assert theItem.itemStatus == "Finished"
|
||||
|
||||
# Expanded
|
||||
theItem.setExpanded(8)
|
||||
assert not theItem.isExpanded
|
||||
@@ -73,6 +94,20 @@ def testItemSettersSimple(nwDummy):
|
||||
theItem.setExpanded(True)
|
||||
assert theItem.isExpanded
|
||||
|
||||
# Exported
|
||||
theItem.setExported(8)
|
||||
assert not theItem.isExported
|
||||
theItem.setExported(None)
|
||||
assert not theItem.isExported
|
||||
theItem.setExported("None")
|
||||
assert not theItem.isExported
|
||||
theItem.setExported("What?")
|
||||
assert not theItem.isExported
|
||||
theItem.setExported("True")
|
||||
assert theItem.isExported
|
||||
theItem.setExported(True)
|
||||
assert theItem.isExported
|
||||
|
||||
# CharCount
|
||||
theItem.setCharCount(None)
|
||||
assert theItem.charCount == 0
|
||||
@@ -105,10 +140,47 @@ def testItemSettersSimple(nwDummy):
|
||||
theItem.setCursorPos(1)
|
||||
assert theItem.cursorPos == 1
|
||||
|
||||
@pytest.mark.project
|
||||
def testItemClassSetter(nwDummy):
|
||||
# Initial Count
|
||||
theItem.setWordCount(234)
|
||||
theItem.saveInitialCount()
|
||||
assert theItem.initCount == 234
|
||||
|
||||
theProject = NWProject(nwDummy)
|
||||
# END Test testCoreItem_Setters
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_TypeSetter(dummyGUI):
|
||||
"""Test the setter for all the nwItemType values for the NWItem
|
||||
class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Type
|
||||
theItem.setType(None)
|
||||
assert theItem.itemType == nwItemType.NO_TYPE
|
||||
theItem.setType("NONSENSE")
|
||||
assert theItem.itemType == nwItemType.NO_TYPE
|
||||
theItem.setType("NO_TYPE")
|
||||
assert theItem.itemType == nwItemType.NO_TYPE
|
||||
theItem.setType("ROOT")
|
||||
assert theItem.itemType == nwItemType.ROOT
|
||||
theItem.setType("FOLDER")
|
||||
assert theItem.itemType == nwItemType.FOLDER
|
||||
theItem.setType("FILE")
|
||||
assert theItem.itemType == nwItemType.FILE
|
||||
theItem.setType("TRASH")
|
||||
assert theItem.itemType == nwItemType.TRASH
|
||||
theItem.setType(nwItemType.ROOT)
|
||||
assert theItem.itemType == nwItemType.ROOT
|
||||
|
||||
# END Test testCoreItem_TypeSetter
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_ClassSetter(dummyGUI):
|
||||
"""Test the setter for all the nwItemClass values for the NWItem
|
||||
class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Class
|
||||
@@ -138,33 +210,17 @@ def testItemClassSetter(nwDummy):
|
||||
assert theItem.itemClass == nwItemClass.ARCHIVE
|
||||
theItem.setClass("TRASH")
|
||||
assert theItem.itemClass == nwItemClass.TRASH
|
||||
theItem.setClass(nwItemClass.NOVEL)
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
|
||||
@pytest.mark.project
|
||||
def testItemTypeSetter(nwDummy):
|
||||
# END Test testCoreItem_ClassSetter
|
||||
|
||||
theProject = NWProject(nwDummy)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Type
|
||||
theItem.setType(None)
|
||||
assert theItem.itemType == nwItemType.NO_TYPE
|
||||
theItem.setType("NONSENSE")
|
||||
assert theItem.itemType == nwItemType.NO_TYPE
|
||||
theItem.setType("NO_TYPE")
|
||||
assert theItem.itemType == nwItemType.NO_TYPE
|
||||
theItem.setType("ROOT")
|
||||
assert theItem.itemType == nwItemType.ROOT
|
||||
theItem.setType("FOLDER")
|
||||
assert theItem.itemType == nwItemType.FOLDER
|
||||
theItem.setType("FILE")
|
||||
assert theItem.itemType == nwItemType.FILE
|
||||
theItem.setType("TRASH")
|
||||
assert theItem.itemType == nwItemType.TRASH
|
||||
|
||||
@pytest.mark.project
|
||||
def testItemLayoutSetter(nwDummy):
|
||||
|
||||
theProject = NWProject(nwDummy)
|
||||
@pytest.mark.core
|
||||
def testCoreItem_LayoutSetter(dummyGUI):
|
||||
"""Test the setter for all the nwItemLayout values for the NWItem
|
||||
class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theItem = NWItem(theProject)
|
||||
|
||||
# Layout
|
||||
@@ -190,14 +246,22 @@ def testItemLayoutSetter(nwDummy):
|
||||
assert theItem.itemLayout == nwItemLayout.SCENE
|
||||
theItem.setLayout("NOTE")
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
theItem.setLayout(nwItemLayout.NOTE)
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
@pytest.mark.project
|
||||
def testItemXMLPackUnpack(nwDummy):
|
||||
# END Test testCoreItem_LayoutSetter
|
||||
|
||||
theProject = NWProject(nwDummy)
|
||||
theItem = NWItem(theProject)
|
||||
@pytest.mark.core
|
||||
def testCoreItem_XMLPackUnpack(dummyGUI):
|
||||
"""Test packing and unpacking XML objects for the NWItem class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
|
||||
# File
|
||||
# ====
|
||||
|
||||
theItem = NWItem(theProject)
|
||||
theItem.setHandle("0123456789abc")
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setOrder(1)
|
||||
@@ -206,7 +270,7 @@ def testItemXMLPackUnpack(nwDummy):
|
||||
theItem.setType("FILE")
|
||||
theItem.setStatus("Main")
|
||||
theItem.setLayout("NOTE")
|
||||
theItem.setExpanded(True)
|
||||
theItem.setExported(False)
|
||||
theItem.setParaCount(3)
|
||||
theItem.setWordCount(5)
|
||||
theItem.setCharCount(7)
|
||||
@@ -219,17 +283,18 @@ def testItemXMLPackUnpack(nwDummy):
|
||||
b"<content>"
|
||||
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
||||
b"<name>A Name</name><type>FILE</type><class>NOVEL</class><status>New</status>"
|
||||
b"<exported>True</exported><layout>NOTE</layout><charCount>7</charCount>"
|
||||
b"<exported>False</exported><layout>NOTE</layout><charCount>7</charCount>"
|
||||
b"<wordCount>5</wordCount><paraCount>3</paraCount><cursorPos>11</cursorPos></item>"
|
||||
b"</content>"
|
||||
)
|
||||
|
||||
# Unpack
|
||||
theItem = NWItem(theProject)
|
||||
assert theItem.unpackXML(xContent[0])
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
assert theItem.itemOrder == 1
|
||||
assert theItem.isExpanded
|
||||
assert theItem.isExported is False
|
||||
assert theItem.paraCount == 3
|
||||
assert theItem.wordCount == 5
|
||||
assert theItem.charCount == 7
|
||||
@@ -238,6 +303,52 @@ def testItemXMLPackUnpack(nwDummy):
|
||||
assert theItem.itemType == nwItemType.FILE
|
||||
assert theItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# Folder
|
||||
# ======
|
||||
|
||||
theItem = NWItem(theProject)
|
||||
theItem.setHandle("0123456789abc")
|
||||
theItem.setParent("0123456789abc")
|
||||
theItem.setOrder(1)
|
||||
theItem.setName("A Name")
|
||||
theItem.setClass("NOVEL")
|
||||
theItem.setType("FOLDER")
|
||||
theItem.setStatus("Main")
|
||||
theItem.setLayout("NOTE")
|
||||
theItem.setExpanded(True)
|
||||
theItem.setExported(False)
|
||||
theItem.setParaCount(3)
|
||||
theItem.setWordCount(5)
|
||||
theItem.setCharCount(7)
|
||||
theItem.setCursorPos(11)
|
||||
|
||||
# Pack
|
||||
xContent = etree.SubElement(nwXML, "content")
|
||||
theItem.packXML(xContent)
|
||||
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
|
||||
b"<content>"
|
||||
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
|
||||
b"<name>A Name</name><type>FOLDER</type><class>NOVEL</class><status>New</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"</content>"
|
||||
)
|
||||
|
||||
# Unpack
|
||||
theItem = NWItem(theProject)
|
||||
assert theItem.unpackXML(xContent[0])
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
assert theItem.itemOrder == 1
|
||||
assert theItem.isExpanded is True
|
||||
assert theItem.isExported is True
|
||||
assert theItem.paraCount == 0
|
||||
assert theItem.wordCount == 0
|
||||
assert theItem.charCount == 0
|
||||
assert theItem.cursorPos == 0
|
||||
assert theItem.itemClass == nwItemClass.NOVEL
|
||||
assert theItem.itemType == nwItemType.FOLDER
|
||||
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
# Errors
|
||||
|
||||
## Not an Item
|
||||
@@ -268,3 +379,5 @@ def testItemXMLPackUnpack(nwDummy):
|
||||
assert etree.tostring(xDummy, pretty_print=False, encoding="utf-8") == (
|
||||
b"<group/>"
|
||||
)
|
||||
|
||||
# END Test testCoreItem_XMLPackUnpack
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter OptionState Class Tester
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from dummy import causeOSError
|
||||
|
||||
from nw.core import NWProject
|
||||
from nw.core.options import OptionState
|
||||
from nw.constants import nwFiles
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreOptions_LoadSave(monkeypatch, dummyGUI, tmpDir):
|
||||
"""Test loading and saving from the OptionState class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theOpts = OptionState(theProject)
|
||||
|
||||
# Write a test file
|
||||
optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE)
|
||||
with open(optFile, mode="w+", encoding="utf8") as outFile:
|
||||
json.dump({
|
||||
"GuiBuildNovel": {
|
||||
"winWidth": 1000,
|
||||
"winHeight": 700,
|
||||
"addNovel": True,
|
||||
"addNotes": False,
|
||||
"textFont": "Cantarell",
|
||||
"dummyItem": None,
|
||||
},
|
||||
"DummyGroup": {
|
||||
"dummyItem": None,
|
||||
},
|
||||
}, outFile)
|
||||
|
||||
# Load and save with no path set
|
||||
theProject.projMeta = None
|
||||
assert not theOpts.loadSettings()
|
||||
assert not theOpts.saveSettings()
|
||||
|
||||
# Set path
|
||||
theProject.projMeta = tmpDir
|
||||
assert theProject.projMeta == tmpDir
|
||||
|
||||
# Cause open() to fail
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
assert not theOpts.loadSettings()
|
||||
assert not theOpts.saveSettings()
|
||||
monkeypatch.undo()
|
||||
|
||||
# Load proper
|
||||
assert theOpts.loadSettings()
|
||||
|
||||
# Check that unwanted items have been removed
|
||||
assert theOpts.theState == {
|
||||
"GuiBuildNovel": {
|
||||
"winWidth": 1000,
|
||||
"winHeight": 700,
|
||||
"addNovel": True,
|
||||
"addNotes": False,
|
||||
"textFont": "Cantarell",
|
||||
},
|
||||
}
|
||||
|
||||
# Save proper
|
||||
assert theOpts.saveSettings()
|
||||
|
||||
# Load again to check we get the values back
|
||||
assert theOpts.loadSettings()
|
||||
assert theOpts.theState == {
|
||||
"GuiBuildNovel": {
|
||||
"winWidth": 1000,
|
||||
"winHeight": 700,
|
||||
"addNovel": True,
|
||||
"addNotes": False,
|
||||
"textFont": "Cantarell",
|
||||
},
|
||||
}
|
||||
|
||||
# END Test testCoreOptions_LoadSave
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreOptions_SetGet(monkeypatch, dummyGUI, tmpDir):
|
||||
"""Test setting and getting values from the OptionState class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theOpts = OptionState(theProject)
|
||||
|
||||
# Set invalid values
|
||||
assert not theOpts.setValue("DummyGroup", "dummyItem", None)
|
||||
assert not theOpts.setValue("GuiBuildNovel", "dummyItem", None)
|
||||
|
||||
# Set valid value
|
||||
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
|
||||
|
||||
# Set some values of different types
|
||||
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
|
||||
assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34)
|
||||
assert theOpts.setValue("GuiBuildNovel", "addNovel", True)
|
||||
assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell")
|
||||
|
||||
# Generic get, doesn't check type
|
||||
assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100
|
||||
assert theOpts.getValue("GuiBuildNovel", "winHeight", None) == 12.34
|
||||
assert theOpts.getValue("GuiBuildNovel", "addNovel", None) is True
|
||||
assert theOpts.getValue("GuiBuildNovel", "textFont", None) == "Cantarell"
|
||||
assert theOpts.getValue("GuiBuildNovel", "dummyItem", None) is None
|
||||
|
||||
# Get type-specific
|
||||
assert theOpts.getString("GuiBuildNovel", "winWidth", None) == "100"
|
||||
assert theOpts.getString("GuiBuildNovel", "dummyItem", None) is None
|
||||
assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100
|
||||
assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None
|
||||
assert theOpts.getInt("GuiBuildNovel", "dummyItem", None) is None
|
||||
assert theOpts.getFloat("GuiBuildNovel", "winWidth", None) == 100.0
|
||||
assert theOpts.getFloat("GuiBuildNovel", "textFont", None) is None
|
||||
assert theOpts.getFloat("GuiBuildNovel", "dummyItem", None) is None
|
||||
assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True
|
||||
assert theOpts.getBool("GuiBuildNovel", "dummyItem", None) is None
|
||||
|
||||
# Check integer validators
|
||||
assert theOpts.validIntRange(5, 0, 9, 3) == 5
|
||||
assert theOpts.validIntRange(5, 0, 4, 3) == 3
|
||||
assert theOpts.validIntRange(5, 0, 5, 3) == 5
|
||||
assert theOpts.validIntRange(0, 0, 5, 3) == 0
|
||||
|
||||
assert theOpts.validIntTuple(0, (0, 1, 2), 3) == 0
|
||||
assert theOpts.validIntTuple(5, (0, 1, 2), 3) == 3
|
||||
|
||||
# END Test testCoreOptions_SetGet
|
||||
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Spell Check Class Tester
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from dummy import causeOSError
|
||||
from tools import readFile, writeFile
|
||||
|
||||
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
|
||||
from nw.constants import nwConst
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreSpell_Super(monkeypatch, tmpDir, tmpConf):
|
||||
"""Test the spell checker super class
|
||||
"""
|
||||
wList = os.path.join(tmpDir, "wordlist.txt")
|
||||
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||
|
||||
spChk = NWSpellCheck()
|
||||
spChk.mainConf = tmpConf
|
||||
|
||||
# Check that dummy functions return results that reflects that spell
|
||||
# checking is effectively disabled
|
||||
assert spChk.setLanguage("", "") is None
|
||||
assert spChk.checkWord("")
|
||||
assert spChk.suggestWords("") == []
|
||||
assert spChk.listDictionaries() == []
|
||||
assert spChk.describeDict() == ("", "")
|
||||
|
||||
# Check language info
|
||||
assert NWSpellCheck.expandLanguage("en") == "English"
|
||||
assert NWSpellCheck.expandLanguage("en_GB") == "English (GB)"
|
||||
|
||||
# Add a word to the user's dictionary
|
||||
assert spChk._readProjectDictionary("dummy") is False
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
assert spChk._readProjectDictionary(wList) is False
|
||||
monkeypatch.undo()
|
||||
assert spChk._readProjectDictionary(wList) is True
|
||||
assert spChk.projectDict == wList
|
||||
|
||||
# Cannot write to file
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
assert spChk.addWord("d_word") is False
|
||||
monkeypatch.undo()
|
||||
assert readFile(wList) == "a_word\nb_word\nc_word\n"
|
||||
|
||||
# First time, OK
|
||||
assert spChk.addWord("d_word") is True
|
||||
assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n"
|
||||
|
||||
# But not added twice
|
||||
assert spChk.addWord("d_word") is False
|
||||
assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n"
|
||||
|
||||
# END Test testCoreSpell_Super
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreSpell_Enchant(monkeypatch, tmpDir, tmpConf):
|
||||
"""Test the pyenchant spell checker
|
||||
"""
|
||||
wList = os.path.join(tmpDir, "wordlist.txt")
|
||||
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||
|
||||
# Block the enchant package (and trigger the dummy class)
|
||||
monkeypatch.setitem(sys.modules, "enchant", None)
|
||||
spChk = NWSpellEnchant()
|
||||
|
||||
spChk.setLanguage("en", wList)
|
||||
assert spChk.setLanguage("", "") is None
|
||||
assert spChk.checkWord("")
|
||||
assert spChk.suggestWords("") == []
|
||||
assert spChk.listDictionaries() == []
|
||||
assert spChk.describeDict() == ("", "")
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
# Load the proper enchant package
|
||||
spChk = NWSpellEnchant()
|
||||
spChk.mainConf = tmpConf
|
||||
spChk.setLanguage("en", wList)
|
||||
|
||||
assert spChk.checkWord("a_word")
|
||||
assert spChk.checkWord("b_word")
|
||||
assert spChk.checkWord("c_word")
|
||||
assert not spChk.checkWord("d_word")
|
||||
|
||||
spChk.addWord("d_word")
|
||||
assert spChk.checkWord("d_word")
|
||||
|
||||
wSuggest = spChk.suggestWords("wrod")
|
||||
assert len(wSuggest) > 0
|
||||
assert "word" in wSuggest
|
||||
|
||||
dList = spChk.listDictionaries()
|
||||
assert len(dList) > 0
|
||||
|
||||
aTag, aName = spChk.describeDict()
|
||||
assert aTag == "en"
|
||||
assert aName != ""
|
||||
|
||||
# END Test testCoreSpell_Enchant
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreSpell_Simple(monkeypatch, tmpDir, tmpConf):
|
||||
"""Test the fallback simple spell checker
|
||||
"""
|
||||
wList = os.path.join(tmpDir, "wordlist.txt")
|
||||
wDict = os.path.join(tmpDir, "en.dict")
|
||||
writeFile(wList, "a_word\nb_word\nc_word\n")
|
||||
writeFile(wDict, "# Comment\ne_word\nf_word\ng_word\n")
|
||||
|
||||
spChk = NWSpellSimple()
|
||||
spChk.mainConf = tmpConf
|
||||
spChk.mainConf.dictPath = tmpDir
|
||||
|
||||
# Load dictionary, but fail
|
||||
monkeypatch.setattr("builtins.open", causeOSError)
|
||||
spChk.setLanguage("en", wList)
|
||||
assert spChk.spellLanguage is None
|
||||
assert spChk.WORDS == spChk.projDict
|
||||
monkeypatch.undo()
|
||||
|
||||
# Load dictionary properly
|
||||
spChk.setLanguage("en", wList)
|
||||
assert spChk.projDict == ["a_word", "b_word", "c_word"]
|
||||
assert spChk.WORDS == ["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"]
|
||||
|
||||
# Check words
|
||||
assert spChk.checkWord("a_word")
|
||||
assert spChk.checkWord("b_word")
|
||||
assert spChk.checkWord("c_word")
|
||||
assert not spChk.checkWord("d_word")
|
||||
assert spChk.checkWord("e_word")
|
||||
assert spChk.checkWord("f_word")
|
||||
assert spChk.checkWord("g_word")
|
||||
|
||||
# Add word
|
||||
spChk.addWord("d_word")
|
||||
assert spChk.checkWord("d_word")
|
||||
|
||||
# Check spelling
|
||||
assert spChk.suggestWords(" \t") == []
|
||||
|
||||
wSuggest = spChk.suggestWords("d_wrod")
|
||||
assert len(wSuggest) > 0
|
||||
assert "d_word" in wSuggest
|
||||
|
||||
# Break the matching
|
||||
monkeypatch.setattr("difflib.get_close_matches", lambda *args, **kwargs: [""])
|
||||
assert spChk.suggestWords("word") == []
|
||||
monkeypatch.undo()
|
||||
|
||||
# Capitalisation
|
||||
wSuggest = spChk.suggestWords("D_wrod")
|
||||
assert len(wSuggest) > 0
|
||||
assert "D_word" in wSuggest
|
||||
|
||||
# List dictionaries
|
||||
assert spChk.listDictionaries() == [("en", "English [%s]" % nwConst.SP_INTERNAL)]
|
||||
|
||||
# Description
|
||||
aTag, aName = spChk.describeDict()
|
||||
assert aTag == "en"
|
||||
assert aName == nwConst.SP_INTERNAL
|
||||
|
||||
# END Test testCoreSpell_Simple
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Status Class Tester
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nw.core.status import NWStatus
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_Entries():
|
||||
"""Test all the simple setters for the NWItem class.
|
||||
"""
|
||||
theStatus = NWStatus()
|
||||
|
||||
# Add entries
|
||||
theStatus.addEntry("New", (100, 100, 100))
|
||||
theStatus.addEntry("Minor", (200, 50, 0))
|
||||
theStatus.addEntry("Major", (200, 150, 0))
|
||||
theStatus.addEntry("Main", (50, 200, 0))
|
||||
|
||||
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
|
||||
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
|
||||
assert theStatus._theCounts == [0, 0, 0, 0]
|
||||
assert theStatus._theMap["New"] == 0
|
||||
assert theStatus._theMap["Minor"] == 1
|
||||
assert theStatus._theMap["Major"] == 2
|
||||
assert theStatus._theMap["Main"] == 3
|
||||
assert theStatus._theLength == 4
|
||||
|
||||
# Lookups
|
||||
assert theStatus.lookupEntry(None) is None
|
||||
assert theStatus.lookupEntry("dummy") is None
|
||||
assert theStatus.lookupEntry("Main") == 3
|
||||
|
||||
# Checks
|
||||
assert theStatus.checkEntry(123) == "New"
|
||||
assert theStatus.checkEntry("Stuff") == "New"
|
||||
assert theStatus.checkEntry("New ") == "New"
|
||||
assert theStatus.checkEntry(" Main ") == "Main"
|
||||
|
||||
# Set new list
|
||||
newList = [
|
||||
("New", 1, 1, 1, "New"),
|
||||
("Minor", 2, 2, 2, "Minor"),
|
||||
("Major", 3, 3, 3, "Major"),
|
||||
("Min", 4, 4, 4, "Main"),
|
||||
("Max", 5, 5, 5, None),
|
||||
]
|
||||
assert theStatus.setNewEntries(None) == {}
|
||||
assert theStatus.setNewEntries(newList) == {"Main": "Min"}
|
||||
|
||||
assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"]
|
||||
assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)]
|
||||
assert theStatus._theCounts == [0, 0, 0, 0, 0]
|
||||
assert theStatus._theMap["New"] == 0
|
||||
assert theStatus._theMap["Minor"] == 1
|
||||
assert theStatus._theMap["Major"] == 2
|
||||
assert theStatus._theMap["Min"] == 3
|
||||
assert theStatus._theMap["Max"] == 4
|
||||
assert theStatus._theLength == 5
|
||||
|
||||
# Add counts
|
||||
countTo = [3, 5, 7, 9, 11]
|
||||
for i, n in enumerate(countTo):
|
||||
for _ in range(n):
|
||||
theStatus.countEntry(theStatus._theLabels[i])
|
||||
assert theStatus._theCounts == countTo
|
||||
|
||||
# Iterate
|
||||
for i, (sA, sB, sC) in enumerate(theStatus):
|
||||
assert sA == theStatus._theLabels[i]
|
||||
assert sB == theStatus._theColours[i]
|
||||
assert sC == theStatus._theCounts[i]
|
||||
|
||||
assert theStatus[9] == (None, None, None)
|
||||
|
||||
# Clear counts
|
||||
theStatus.resetCounts()
|
||||
assert theStatus._theCounts == [0, 0, 0, 0, 0]
|
||||
|
||||
# END Test testCoreStatus_Entries
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_XMLPackUnpack():
|
||||
"""Test all the simple setters for the NWItem class.
|
||||
"""
|
||||
theStatus = NWStatus()
|
||||
theStatus.addEntry("New", (100, 100, 100))
|
||||
theStatus.addEntry("Minor", (200, 50, 0))
|
||||
theStatus.addEntry("Major", (200, 150, 0))
|
||||
theStatus.addEntry("Main", (50, 200, 0))
|
||||
|
||||
countTo = [3, 5, 7, 9]
|
||||
for i, n in enumerate(countTo):
|
||||
for _ in range(n):
|
||||
theStatus.countEntry(theStatus._theLabels[i])
|
||||
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
|
||||
# Pack
|
||||
xStatus = etree.SubElement(nwXML, "status")
|
||||
theStatus.packXML(xStatus)
|
||||
assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == (
|
||||
b"<status>"
|
||||
b"<entry blue=\"100\" green=\"100\" red=\"100\">New</entry>"
|
||||
b"<entry blue=\"0\" green=\"50\" red=\"200\">Minor</entry>"
|
||||
b"<entry blue=\"0\" green=\"150\" red=\"200\">Major</entry>"
|
||||
b"<entry blue=\"0\" green=\"200\" red=\"50\">Main</entry>"
|
||||
b"</status>"
|
||||
)
|
||||
|
||||
# Unpack
|
||||
theStatus = NWStatus()
|
||||
assert theStatus.unpackXML(xStatus)
|
||||
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
|
||||
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
|
||||
assert theStatus._theCounts == [0, 0, 0, 0]
|
||||
assert theStatus._theMap["New"] == 0
|
||||
assert theStatus._theMap["Minor"] == 1
|
||||
assert theStatus._theMap["Major"] == 2
|
||||
assert theStatus._theMap["Main"] == 3
|
||||
assert theStatus._theLength == 4
|
||||
|
||||
# END Test testCoreStatus_XMLPackUnpack
|
||||
@@ -0,0 +1,373 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter ToHtml Class Tester
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from nw.core import NWProject, NWIndex, ToHtml
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToHtml_Format(dummyGUI):
|
||||
"""Test all the formatters for the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
|
||||
theHtml = ToHtml(theProject, dummyGUI)
|
||||
|
||||
# Export Mode
|
||||
# ===========
|
||||
|
||||
assert theHtml._formatSynopsis("synopsis text") == (
|
||||
"<p class='synopsis'><strong>Synopsis: </strong>synopsis text</p>\n"
|
||||
)
|
||||
assert theHtml._formatComments("comment text") == (
|
||||
"<p class='comment'><strong>Comment: </strong>comment text</p>\n"
|
||||
)
|
||||
|
||||
assert theHtml._formatKeywords("") == ""
|
||||
assert theHtml._formatKeywords("tag: Jane") == (
|
||||
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>"
|
||||
)
|
||||
assert theHtml._formatKeywords("char: Bod, Jane") == (
|
||||
"<div>"
|
||||
"<span class='tags'>Characters:</span> "
|
||||
"<a href='#tag_Bod'>Bod</a>, "
|
||||
"<a href='#tag_Jane'>Jane</a>"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
# Preview Mode
|
||||
# ============
|
||||
|
||||
theHtml.setPreview(True, True)
|
||||
|
||||
assert theHtml._formatSynopsis("synopsis text") == (
|
||||
"<p class='comment'><span class='synopsis'>Synopsis: </span>synopsis text</p>\n"
|
||||
)
|
||||
assert theHtml._formatComments("comment text") == (
|
||||
"<p class='comment'>comment text</p>\n"
|
||||
)
|
||||
|
||||
assert theHtml._formatKeywords("") == ""
|
||||
assert theHtml._formatKeywords("tag: Jane") == (
|
||||
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>"
|
||||
)
|
||||
assert theHtml._formatKeywords("char: Bod, Jane") == (
|
||||
"<div>"
|
||||
"<span class='tags'>Characters:</span> "
|
||||
"<a href='#char=Bod'>Bod</a>, "
|
||||
"<a href='#char=Jane'>Jane</a>"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
# END Test testCoreToHtml_Format
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToHtml_Convert(dummyGUI):
|
||||
"""Test the converter of the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
|
||||
theHtml = ToHtml(theProject, dummyGUI)
|
||||
|
||||
# Export Mode
|
||||
# ===========
|
||||
|
||||
theHtml.isNovel = True
|
||||
|
||||
# Header 1
|
||||
theHtml.theText = "# Title\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h1 class='title'>Title</h1>\n"
|
||||
|
||||
# Header 2
|
||||
theHtml.theText = "## Chapter Title\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h1>Chapter Title</h1>\n"
|
||||
|
||||
# Header 3
|
||||
theHtml.theText = "### Scene Title\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h2>Scene Title</h2>\n"
|
||||
|
||||
# Header 4
|
||||
theHtml.theText = "#### Section Title\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h3>Section Title</h3>\n"
|
||||
|
||||
theHtml.isNovel = False
|
||||
theHtml.setLinkHeaders(True)
|
||||
|
||||
# Header 1
|
||||
theHtml.theText = "# Heading One\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h1><a name='T000001'></a>Heading One</h1>\n"
|
||||
|
||||
# Header 2
|
||||
theHtml.theText = "## Heading Two\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h2><a name='T000001'></a>Heading Two</h2>\n"
|
||||
|
||||
# Header 3
|
||||
theHtml.theText = "### Heading Three\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h3><a name='T000001'></a>Heading Three</h3>\n"
|
||||
|
||||
# Header 4
|
||||
theHtml.theText = "#### Heading Four\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<h4><a name='T000001'></a>Heading Four</h4>\n"
|
||||
|
||||
# Text
|
||||
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<p>Some <strong>nested bold and <em>italic</em> and "
|
||||
"<del>strikethrough</del> text</strong> here</p>\n"
|
||||
)
|
||||
|
||||
# Text w/Hard Break
|
||||
theHtml.theText = "Line one \nLine two \nLine three\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<p class='break'>Line one<br/>Line two<br/>Line three</p>\n"
|
||||
)
|
||||
|
||||
# Synopsis
|
||||
theHtml.theText = "%synopsis: The synopsis ...\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == ""
|
||||
|
||||
theHtml.setSynopsis(True)
|
||||
theHtml.theText = "%synopsis: The synopsis ...\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<p class='synopsis'><strong>Synopsis: </strong>The synopsis ...</p>\n"
|
||||
)
|
||||
|
||||
# Comment
|
||||
theHtml.theText = "% A comment ...\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == ""
|
||||
|
||||
theHtml.setComments(True)
|
||||
theHtml.theText = "% A comment ...\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<p class='comment'><strong>Comment: </strong>A comment ...</p>\n"
|
||||
)
|
||||
|
||||
# Keywords
|
||||
theHtml.theText = "@char: Bod, Jane\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == ""
|
||||
|
||||
theHtml.setKeywords(True)
|
||||
theHtml.theText = "@char: Bod, Jane\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<div><span class='tags'>Characters:</span> "
|
||||
"<a href='#tag_Bod'>Bod</a>, <a href='#tag_Jane'>Jane</a></div>"
|
||||
)
|
||||
|
||||
# Direct Tests
|
||||
# ============
|
||||
|
||||
theHtml.isNovel = True
|
||||
|
||||
# Title
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE),
|
||||
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' style='text-align: center; page-break-before: never;'>"
|
||||
"<a name='T000001'></a>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Separator
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_SEP, 1, "* * *", None, theHtml.A_CENTRE),
|
||||
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<p class='sep'>* * *</p>\n"
|
||||
|
||||
# Skip
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_SKIP, 1, "", None, theHtml.A_NONE),
|
||||
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == "<p class='skip'> </p>\n"
|
||||
|
||||
# Styles
|
||||
# ======
|
||||
|
||||
theHtml.setLinkHeaders(False)
|
||||
|
||||
# Align Left
|
||||
theHtml.setStyles(False)
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
theHtml.setStyles(True)
|
||||
|
||||
# Align Left
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' style='text-align: left;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Align Right
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_RIGHT),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' style='text-align: right;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Align Centre
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_CENTRE),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' style='text-align: center;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Align Justify
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_JUSTIFY),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' style='text-align: justify;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Page Break Always
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB | theHtml.A_PBA),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' "
|
||||
"style='page-break-before: always; page-break-after: always;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Page Break Avoid
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' "
|
||||
"style='page-break-before: avoid; page-break-after: avoid;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Page Break ANever
|
||||
theHtml.theTokens = [
|
||||
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO),
|
||||
]
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<h1 class='title' "
|
||||
"style='page-break-before: never; page-break-after: never;'>A Title</h1>\n"
|
||||
)
|
||||
|
||||
# Preview Mode
|
||||
# ============
|
||||
|
||||
theHtml.setPreview(True, True)
|
||||
|
||||
# Text (HTML4)
|
||||
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<p>Some <b>nested bold and <i>italic</i> and "
|
||||
"<span style='text-decoration: line-through;'>strikethrough</span> "
|
||||
"text</b> here</p>\n"
|
||||
)
|
||||
|
||||
# END Test testCoreToHtml_Convert
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToHtml_Methods(dummyGUI):
|
||||
"""Test all the other methods of the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theHtml = ToHtml(theProject, dummyGUI)
|
||||
|
||||
# Auto-Replace
|
||||
docText = "Text with <brackets> & short–dash, long—dash …\n"
|
||||
theHtml.theText = docText
|
||||
theHtml.doAutoReplace()
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theResult == (
|
||||
"<p>Text with <brackets> & short–dash, long—dash …</p>\n"
|
||||
)
|
||||
|
||||
# Revert on MD
|
||||
assert theHtml.theMarkdown == (
|
||||
"Text with <brackets> & short–dash, long—dash …\n\n"
|
||||
)
|
||||
theHtml.doPostProcessing()
|
||||
assert theHtml.theMarkdown == docText + "\n"
|
||||
|
||||
# With Preview, No Revert
|
||||
theHtml.setPreview(True, True)
|
||||
theHtml.theText = docText
|
||||
theHtml.doAutoReplace()
|
||||
theHtml.tokenizeText()
|
||||
theHtml.doConvert()
|
||||
assert theHtml.theMarkdown == (
|
||||
"Text with <brackets> & short–dash, long—dash …\n\n"
|
||||
)
|
||||
theHtml.doPostProcessing()
|
||||
assert theHtml.theMarkdown == (
|
||||
"Text with <brackets> & short–dash, long—dash …\n\n"
|
||||
)
|
||||
|
||||
# CSS
|
||||
# ===
|
||||
|
||||
assert len(theHtml.getStyleSheet()) > 1
|
||||
assert "p {text-align: left;}" in theHtml.getStyleSheet()
|
||||
assert "p {text-align: justify;}" not in theHtml.getStyleSheet()
|
||||
|
||||
theHtml.setJustify(True)
|
||||
assert "p {text-align: left;}" not in theHtml.getStyleSheet()
|
||||
assert "p {text-align: justify;}" in theHtml.getStyleSheet()
|
||||
|
||||
theHtml.setStyles(False)
|
||||
assert theHtml.getStyleSheet() == []
|
||||
|
||||
# END Test testCoreToHtml_Methods
|
||||
@@ -0,0 +1,644 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Tokenizer Class Tester
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from nw.core import NWProject, NWDoc
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_Setters(dummyGUI):
|
||||
"""Test all the setters for the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theToken = Tokenizer(theProject, dummyGUI)
|
||||
|
||||
# Verify defaults
|
||||
assert theToken.fmtTitle == "%title%"
|
||||
assert theToken.fmtChapter == "%title%"
|
||||
assert theToken.fmtUnNum == "%title%"
|
||||
assert theToken.fmtScene == "%title%"
|
||||
assert theToken.fmtSection == "%title%"
|
||||
assert theToken.hideScene is False
|
||||
assert theToken.hideSection is False
|
||||
assert theToken.linkHeaders is False
|
||||
assert theToken.doBodyText is True
|
||||
assert theToken.doSynopsis is False
|
||||
assert theToken.doComments is False
|
||||
assert theToken.doKeywords is False
|
||||
assert theToken.doJustify is False
|
||||
|
||||
# Set new values
|
||||
theToken.setTitleFormat("T: %title%")
|
||||
theToken.setChapterFormat("C: %title%")
|
||||
theToken.setUnNumberedFormat("U: %title%")
|
||||
theToken.setSceneFormat("S: %title%", True)
|
||||
theToken.setSectionFormat("X: %title%", True)
|
||||
theToken.setLinkHeaders(True)
|
||||
theToken.setBodyText(False)
|
||||
theToken.setSynopsis(True)
|
||||
theToken.setComments(True)
|
||||
theToken.setKeywords(True)
|
||||
theToken.setJustify(True)
|
||||
|
||||
# Check new values
|
||||
assert theToken.fmtTitle == "T: %title%"
|
||||
assert theToken.fmtChapter == "C: %title%"
|
||||
assert theToken.fmtUnNum == "U: %title%"
|
||||
assert theToken.fmtScene == "S: %title%"
|
||||
assert theToken.fmtSection == "X: %title%"
|
||||
assert theToken.hideScene is True
|
||||
assert theToken.hideSection is True
|
||||
assert theToken.linkHeaders is True
|
||||
assert theToken.doBodyText is False
|
||||
assert theToken.doSynopsis is True
|
||||
assert theToken.doComments is True
|
||||
assert theToken.doKeywords is True
|
||||
assert theToken.doJustify is True
|
||||
|
||||
# END Test testCoreToken_Setters
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
|
||||
"""Test handling files and text in the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theProject.projTree.setSeed(42)
|
||||
theToken = Tokenizer(theProject, dummyGUI)
|
||||
|
||||
assert theProject.openProject(nwMinimal)
|
||||
sHandle = "8c659a11cd429"
|
||||
|
||||
# Set some content to work with
|
||||
|
||||
docText = (
|
||||
"### Scene Six\n\n"
|
||||
"This is text with _italic text_, some **bold text**, some ~~deleted text~~, "
|
||||
"and some **_mixed text_** and **some _nested_ text**.\n\n"
|
||||
"#### Replace\n\n"
|
||||
"Also, replace <A> and <B>.\n\n"
|
||||
)
|
||||
docTextR = docText.replace("<A>", "this").replace("<B>", "that")
|
||||
|
||||
nDoc = NWDoc(theProject, dummyGUI)
|
||||
nDoc.openDocument(sHandle)
|
||||
nDoc.saveDocument(docText)
|
||||
nDoc.clearDocument()
|
||||
|
||||
theProject.setAutoReplace({"A": "this", "B": "that"})
|
||||
|
||||
assert theProject.saveProject()
|
||||
|
||||
# Root heading
|
||||
assert theToken.addRootHeading("dummy") is False
|
||||
assert theToken.addRootHeading(sHandle) is False
|
||||
assert theToken.addRootHeading("7695ce551d265") is True
|
||||
assert theToken.theMarkdown == "# Notes: Plot\n\n"
|
||||
|
||||
# Set text
|
||||
assert theToken.setText("dummy") is False
|
||||
assert theToken.setText(sHandle) is True
|
||||
assert theToken.theText == docText
|
||||
|
||||
monkeypatch.setattr("nw.constants.nwConst.MAX_DOCSIZE", 100)
|
||||
assert theToken.setText(sHandle, docText) is True
|
||||
assert theToken.theText == (
|
||||
"# ERROR\n\n"
|
||||
"Document 'New Scene' is too big (0.00 MB). Skipping.\n\n"
|
||||
)
|
||||
monkeypatch.undo()
|
||||
|
||||
assert theToken.setText(sHandle, docText) is True
|
||||
assert theToken.theText == docText
|
||||
|
||||
assert theToken.isNone is False
|
||||
assert theToken.isTitle is False
|
||||
assert theToken.isBook is False
|
||||
assert theToken.isPage is False
|
||||
assert theToken.isPart is False
|
||||
assert theToken.isUnNum is False
|
||||
assert theToken.isChap is False
|
||||
assert theToken.isScene is True
|
||||
assert theToken.isNote is False
|
||||
assert theToken.isNovel is True
|
||||
|
||||
# Auto replace
|
||||
theToken.doAutoReplace()
|
||||
assert theToken.theText == docTextR
|
||||
|
||||
# Access
|
||||
assert theToken.getResult() is None
|
||||
assert theToken.getResultSize() == 0
|
||||
theToken.theResult = ""
|
||||
assert theToken.getResultSize() == 0
|
||||
|
||||
# Post Processing
|
||||
theToken.theResult = r"This is text with escapes: \** \~~ \__"
|
||||
theToken.doPostProcessing()
|
||||
assert theToken.theResult == "This is text with escapes: ** ~~ __"
|
||||
|
||||
# END Test testCoreToken_TextOps
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_Tokenize(dummyGUI):
|
||||
"""Test the tokenization of the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theToken = Tokenizer(theProject, dummyGUI)
|
||||
|
||||
# Header 1
|
||||
theToken.theText = "# Novel Title\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "# Novel Title\n\n"
|
||||
|
||||
# Header 2
|
||||
theToken.theText = "## Chapter One\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "## Chapter One\n\n"
|
||||
|
||||
# Header 3
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "### Scene One\n\n"
|
||||
|
||||
# Header 4
|
||||
theToken.theText = "#### A Section\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "#### A Section\n\n"
|
||||
|
||||
# Comment
|
||||
theToken.theText = "% A comment\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "\n"
|
||||
|
||||
theToken.setComments(True)
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theMarkdown == "% A comment\n\n"
|
||||
|
||||
# Symopsis
|
||||
theToken.theText = "%synopsis: The synopsis\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
theToken.theText = "% synopsis: The synopsis\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "\n"
|
||||
|
||||
theToken.setSynopsis(True)
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theMarkdown == "% synopsis: The synopsis\n\n"
|
||||
|
||||
# Keyword
|
||||
theToken.theText = "@char: Bod\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "\n"
|
||||
|
||||
theToken.setKeywords(True)
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theMarkdown == "@char: Bod\n\n"
|
||||
|
||||
# Text
|
||||
theToken.theText = "Some plain text\non two lines\n\n\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_TEXT, 1, "Some plain text", [], Tokenizer.A_NONE),
|
||||
(Tokenizer.T_TEXT, 2, "on two lines", [], Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "Some plain text\non two lines\n\n\n\n"
|
||||
|
||||
theToken.setBodyText(False)
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "\n\n\n"
|
||||
theToken.setBodyText(True)
|
||||
|
||||
# Text Emphasis
|
||||
theToken.theText = "Some **bolded text** on this lines\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 1,
|
||||
"Some **bolded text** on this lines",
|
||||
[
|
||||
[5, 2, Tokenizer.FMT_B_B],
|
||||
[18, 2, Tokenizer.FMT_B_E],
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "Some **bolded text** on this lines\n\n"
|
||||
|
||||
theToken.theText = "Some _italic text_ on this lines\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 1,
|
||||
"Some _italic text_ on this lines",
|
||||
[
|
||||
[5, 1, Tokenizer.FMT_I_B],
|
||||
[17, 1, Tokenizer.FMT_I_E],
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "Some _italic text_ on this lines\n\n"
|
||||
|
||||
theToken.theText = "Some **_bold italic text_** on this lines\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 1,
|
||||
"Some **_bold italic text_** on this lines",
|
||||
[
|
||||
[5, 2, Tokenizer.FMT_B_B],
|
||||
[7, 1, Tokenizer.FMT_I_B],
|
||||
[24, 1, Tokenizer.FMT_I_E],
|
||||
[25, 2, Tokenizer.FMT_B_E],
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "Some **_bold italic text_** on this lines\n\n"
|
||||
|
||||
theToken.theText = "Some ~~strikethrough text~~ on this lines\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 1,
|
||||
"Some ~~strikethrough text~~ on this lines",
|
||||
[
|
||||
[5, 2, Tokenizer.FMT_D_B],
|
||||
[25, 2, Tokenizer.FMT_D_E],
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == "Some ~~strikethrough text~~ on this lines\n\n"
|
||||
|
||||
theToken.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||
theToken.tokenizeText()
|
||||
assert theToken.theTokens == [
|
||||
(
|
||||
Tokenizer.T_TEXT, 1,
|
||||
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here",
|
||||
[
|
||||
[5, 2, Tokenizer.FMT_B_B],
|
||||
[23, 1, Tokenizer.FMT_I_B],
|
||||
[30, 1, Tokenizer.FMT_I_E],
|
||||
[36, 2, Tokenizer.FMT_D_B],
|
||||
[51, 2, Tokenizer.FMT_D_E],
|
||||
[58, 2, Tokenizer.FMT_B_E],
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
assert theToken.theMarkdown == (
|
||||
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
|
||||
)
|
||||
|
||||
# Check the markdown function as well
|
||||
assert theToken.getFilteredMarkdown() == (
|
||||
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
|
||||
)
|
||||
|
||||
# END Test testCoreToken_Tokenize
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_Headers(dummyGUI):
|
||||
"""Test the header and page parser of the Tokenizer class.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theToken = Tokenizer(theProject, dummyGUI)
|
||||
|
||||
# Nothing
|
||||
theToken.theText = "Some text ...\n"
|
||||
assert theToken.doHeaders() is True
|
||||
theToken.isNone = True
|
||||
assert theToken.doHeaders() is False
|
||||
theToken.isNone = False
|
||||
assert theToken.doHeaders() is True
|
||||
theToken.isNote = True
|
||||
assert theToken.doHeaders() is False
|
||||
theToken.isNote = False
|
||||
|
||||
##
|
||||
# Novel
|
||||
##
|
||||
|
||||
theToken.isNovel = True
|
||||
|
||||
# Titles
|
||||
# ======
|
||||
|
||||
# H1: Title
|
||||
theToken.theText = "# Novel Title\n"
|
||||
theToken.setTitleFormat(r"T: %title%")
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD1, 1, "T: Novel Title", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# Chapters
|
||||
# ========
|
||||
|
||||
# H2: Chapter
|
||||
theToken.theText = "## Chapter One\n"
|
||||
theToken.setChapterFormat(r"C: %title%")
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "C: Chapter One", None, Tokenizer.A_PBB),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H2: Unnumbered Chapter
|
||||
theToken.theText = "## Chapter One\n"
|
||||
theToken.setUnNumberedFormat(r"U: %title%")
|
||||
theToken.isUnNum = True
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "U: Chapter One", None, Tokenizer.A_PBB),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H2: Unnumbered Chapter with Star
|
||||
theToken.theText = "## *Prologue\n"
|
||||
theToken.setUnNumberedFormat(r"U: %title%")
|
||||
theToken.isUnNum = False
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "U: Prologue", None, Tokenizer.A_PBB),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H2: Chapter Word Number
|
||||
theToken.theText = "## Chapter\n"
|
||||
theToken.setChapterFormat(r"Chapter %chw%")
|
||||
theToken.numChapter = 0
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_PBB),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H2: Chapter Roman Number Upper Case
|
||||
theToken.theText = "## Chapter\n"
|
||||
theToken.setChapterFormat(r"Chapter %chI%")
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "Chapter II", None, Tokenizer.A_PBB),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H2: Chapter Roman Number Lower Case
|
||||
theToken.theText = "## Chapter\n"
|
||||
theToken.setChapterFormat(r"Chapter %chi%")
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD2, 1, "Chapter iii", None, Tokenizer.A_PBB),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# Scenes
|
||||
# ======
|
||||
|
||||
# H3: Scene w/Title
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.setSceneFormat(r"S: %title%", False)
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD3, 1, "S: Scene One", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene Hidden wo/Format
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.setSceneFormat(r"", True)
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene wo/Format, first
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.setSceneFormat(r"", False)
|
||||
theToken.firstScene = True
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene wo/Format, not first
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.setSceneFormat(r"", False)
|
||||
theToken.firstScene = False
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene Separator, first
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.setSceneFormat(r"* * *", False)
|
||||
theToken.firstScene = True
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene Separator, not first
|
||||
theToken.theText = "### Scene One\n"
|
||||
theToken.setSceneFormat(r"* * *", False)
|
||||
theToken.firstScene = False
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene w/Absolute Number
|
||||
theToken.theText = "### A Scene\n"
|
||||
theToken.setSceneFormat(r"Scene %sca%", False)
|
||||
theToken.numAbsScene = 0
|
||||
theToken.numChScene = 0
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD3, 1, "Scene 1", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H3: Scene w/Chapter Number
|
||||
theToken.theText = "### A Scene\n"
|
||||
theToken.setSceneFormat(r"Scene %ch%.%sc%", False)
|
||||
theToken.numAbsScene = 0
|
||||
theToken.numChScene = 1
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD3, 1, "Scene 3.2", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# Sections
|
||||
# ========
|
||||
|
||||
# H4: Section Hidden wo/Format
|
||||
theToken.theText = "#### A Section\n"
|
||||
theToken.setSectionFormat(r"", True)
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H4: Section Visible wo/Format
|
||||
theToken.theText = "#### A Section\n"
|
||||
theToken.setSectionFormat(r"", False)
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H4: Section w/Format
|
||||
theToken.theText = "#### A Section\n"
|
||||
theToken.setSectionFormat(r"X: %title%", False)
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD4, 1, "X: A Section", None, Tokenizer.A_NONE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# H4: Section Separator
|
||||
theToken.theText = "#### A Section\n"
|
||||
theToken.setSectionFormat(r"* * *", False)
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
|
||||
]
|
||||
|
||||
# Check the first scene detector
|
||||
assert theToken.firstScene is False
|
||||
theToken.firstScene = True
|
||||
assert theToken.firstScene is True
|
||||
theToken.theText = "Some text ...\n"
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.firstScene is False
|
||||
|
||||
##
|
||||
# Title or Partition
|
||||
##
|
||||
|
||||
theToken.isNovel = False
|
||||
|
||||
# H1: Title
|
||||
theToken.theText = "# Novel Title\n"
|
||||
theToken.tokenizeText()
|
||||
theToken.isTitle = True
|
||||
theToken.isPart = False
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
|
||||
]
|
||||
|
||||
# H1: Partition
|
||||
theToken.theText = "# Partition Title\n"
|
||||
theToken.setTitleFormat(r"T: %title%")
|
||||
theToken.tokenizeText()
|
||||
theToken.isTitle = False
|
||||
theToken.isPart = True
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_HEAD1, 1, "Partition Title", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE),
|
||||
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
|
||||
]
|
||||
|
||||
##
|
||||
# Page
|
||||
##
|
||||
|
||||
theToken.isNovel = False
|
||||
theToken.isTitle = False
|
||||
theToken.isPart = False
|
||||
theToken.isPage = True
|
||||
|
||||
# Some Page Text
|
||||
theToken.theText = "Page text\n\nMore text\n"
|
||||
theToken.tokenizeText()
|
||||
theToken.doHeaders()
|
||||
assert theToken.theTokens == [
|
||||
(Tokenizer.T_TEXT, 1, "Page text", [], Tokenizer.A_PBB | Tokenizer.A_LEFT),
|
||||
(Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_LEFT),
|
||||
(Tokenizer.T_TEXT, 3, "More text", [], Tokenizer.A_LEFT),
|
||||
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_LEFT),
|
||||
]
|
||||
|
||||
# END Test testCoreToken_Headers
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
from nw.core.tools import countWords, numberToRoman, numberToWord
|
||||
|
||||
@pytest.mark.core
|
||||
def testCountWords():
|
||||
def testCoreTools_CountWords():
|
||||
"""Test the word counter and the exclusion filers.
|
||||
"""
|
||||
testText = (
|
||||
@@ -26,15 +26,46 @@ def testCountWords():
|
||||
"\n"
|
||||
"\n"
|
||||
"The third paragraph.\n"
|
||||
"\n"
|
||||
"Dashes\u2013and even longer\u2014dashes."
|
||||
)
|
||||
cC, wC, pC = countWords(testText)
|
||||
|
||||
assert cC == 108
|
||||
assert wC == 17
|
||||
assert pC == 3
|
||||
assert cC == 138
|
||||
assert wC == 22
|
||||
assert pC == 4
|
||||
|
||||
# END Test testCoreTools_CountWords
|
||||
|
||||
@pytest.mark.core
|
||||
def testNumberWords():
|
||||
def testCoreTools_RomanNumbers():
|
||||
"""Test conversion of integers to Roman numbers.
|
||||
"""
|
||||
assert numberToRoman(None, False) == "NAN"
|
||||
assert numberToRoman(0, False) == "OOR"
|
||||
assert numberToRoman(1, False) == "I"
|
||||
assert numberToRoman(2, False) == "II"
|
||||
assert numberToRoman(3, False) == "III"
|
||||
assert numberToRoman(4, False) == "IV"
|
||||
assert numberToRoman(5, False) == "V"
|
||||
assert numberToRoman(6, False) == "VI"
|
||||
assert numberToRoman(7, False) == "VII"
|
||||
assert numberToRoman(8, False) == "VIII"
|
||||
assert numberToRoman(9, False) == "IX"
|
||||
assert numberToRoman(10, False) == "X"
|
||||
assert numberToRoman(14, False) == "XIV"
|
||||
assert numberToRoman(42, False) == "XLII"
|
||||
assert numberToRoman(99, False) == "XCIX"
|
||||
assert numberToRoman(142, False) == "CXLII"
|
||||
assert numberToRoman(542, False) == "DXLII"
|
||||
assert numberToRoman(999, False) == "CMXCIX"
|
||||
assert numberToRoman(2010, False) == "MMX"
|
||||
assert numberToRoman(999, True) == "cmxcix"
|
||||
|
||||
# END Test testCoreTools_RomanNumbers
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTools_NumberWords():
|
||||
"""Test the conversion of integer to English words.
|
||||
"""
|
||||
assert numberToWord(0, "en") == "Zero"
|
||||
@@ -70,27 +101,9 @@ def testNumberWords():
|
||||
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"
|
||||
assert numberToRoman(2, False) == "II"
|
||||
assert numberToRoman(3, False) == "III"
|
||||
assert numberToRoman(4, False) == "IV"
|
||||
assert numberToRoman(5, False) == "V"
|
||||
assert numberToRoman(6, False) == "VI"
|
||||
assert numberToRoman(7, False) == "VII"
|
||||
assert numberToRoman(8, False) == "VIII"
|
||||
assert numberToRoman(9, False) == "IX"
|
||||
assert numberToRoman(10, False) == "X"
|
||||
assert numberToRoman(14, False) == "XIV"
|
||||
assert numberToRoman(42, False) == "XLII"
|
||||
assert numberToRoman(99, False) == "XCIX"
|
||||
assert numberToRoman(142, False) == "CXLII"
|
||||
assert numberToRoman(542, False) == "DXLII"
|
||||
assert numberToRoman(999, False) == "CMXCIX"
|
||||
assert numberToRoman(2010, False) == "MMX"
|
||||
assert numberToRoman(999, True) == "cmxcix"
|
||||
# Test out of range values
|
||||
assert numberToWord(12345, "en") == "[Out of Range]"
|
||||
assert numberToWord(-2345, "en") == "[Negative]"
|
||||
assert numberToWord("234", "en") == "[NaN]"
|
||||
|
||||
# END Test testCoreTools_NumberWords
|
||||
@@ -0,0 +1,433 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter NWTree Class Tester
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nw.core.project import NWProject, NWItem, NWTree
|
||||
from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def dummyItems(dummyGUI):
|
||||
"""Create a list of dummy items.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
|
||||
itemA = NWItem(theProject)
|
||||
itemA.itemName = "Novel"
|
||||
itemA.itemType = nwItemType.ROOT
|
||||
itemA.itemClass = nwItemClass.NOVEL
|
||||
itemA.isExpanded = True
|
||||
|
||||
itemB = NWItem(theProject)
|
||||
itemB.itemName = "Act One"
|
||||
itemB.itemType = nwItemType.FOLDER
|
||||
itemB.itemClass = nwItemClass.NOVEL
|
||||
itemB.isExpanded = True
|
||||
|
||||
itemC = NWItem(theProject)
|
||||
itemC.itemName = "Chapter One"
|
||||
itemC.itemType = nwItemType.FILE
|
||||
itemC.itemClass = nwItemClass.NOVEL
|
||||
itemC.itemLayout = nwItemLayout.CHAPTER
|
||||
itemC.charCount = 300
|
||||
itemC.wordCount = 50
|
||||
itemC.paraCount = 2
|
||||
|
||||
itemD = NWItem(theProject)
|
||||
itemD.itemName = "Scene One"
|
||||
itemD.itemType = nwItemType.FILE
|
||||
itemD.itemClass = nwItemClass.NOVEL
|
||||
itemD.itemLayout = nwItemLayout.SCENE
|
||||
itemD.charCount = 3000
|
||||
itemD.wordCount = 500
|
||||
itemD.paraCount = 20
|
||||
|
||||
itemE = NWItem(theProject)
|
||||
itemE.itemName = "Outtakes"
|
||||
itemE.itemType = nwItemType.ROOT
|
||||
itemE.itemClass = nwItemClass.ARCHIVE
|
||||
itemE.isExpanded = False
|
||||
|
||||
itemF = NWItem(theProject)
|
||||
itemF.itemName = "Trash"
|
||||
itemF.itemType = nwItemType.TRASH
|
||||
itemF.itemClass = nwItemClass.TRASH
|
||||
itemF.isExpanded = False
|
||||
|
||||
itemG = NWItem(theProject)
|
||||
itemG.itemName = "Characters"
|
||||
itemG.itemType = nwItemType.ROOT
|
||||
itemG.itemClass = nwItemClass.CHARACTER
|
||||
itemG.isExpanded = True
|
||||
|
||||
itemH = NWItem(theProject)
|
||||
itemH.itemName = "Jane Doe"
|
||||
itemH.itemType = nwItemType.FILE
|
||||
itemH.itemClass = nwItemClass.CHARACTER
|
||||
itemH.itemLayout = nwItemLayout.NOTE
|
||||
itemH.charCount = 2000
|
||||
itemH.wordCount = 400
|
||||
itemH.paraCount = 16
|
||||
|
||||
theItems = [
|
||||
("a000000000001", None, itemA),
|
||||
("b000000000001", "a000000000001", itemB),
|
||||
("c000000000001", "b000000000001", itemC),
|
||||
("c000000000002", "b000000000001", itemD),
|
||||
("a000000000002", None, itemE),
|
||||
("a000000000003", None, itemF),
|
||||
("a000000000004", None, itemG),
|
||||
("b000000000002", "a000000000002", itemH),
|
||||
]
|
||||
|
||||
return theItems
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_BuildTree(dummyGUI, dummyItems):
|
||||
"""Test building a project tree from a list of items.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
theTree.setSeed(42)
|
||||
assert theTree._handleSeed == 42
|
||||
|
||||
# Check that tree is empty (calls NWTree.__bool__)
|
||||
assert not theTree
|
||||
|
||||
# Check for archive and trash folders
|
||||
assert theTree.trashRoot() is None
|
||||
assert theTree.archiveRoot() is None
|
||||
assert not theTree.isTrashRoot("a000000000003")
|
||||
|
||||
aHandles = []
|
||||
for tHandle, pHande, nwItem in dummyItems:
|
||||
aHandles.append(tHandle)
|
||||
assert theTree.append(tHandle, pHande, nwItem)
|
||||
|
||||
assert theTree._treeChanged
|
||||
|
||||
# Check that tree is not empty (calls __bool__)
|
||||
assert theTree
|
||||
|
||||
# Check the number of elements (calls __len__)
|
||||
assert len(theTree) == len(dummyItems)
|
||||
|
||||
# Check that we have the correct handles
|
||||
assert theTree.handles() == aHandles
|
||||
|
||||
# Check by iterator (calls __iter__, __next__ and __getitem__)
|
||||
for theItem, theHandle in zip(theTree, aHandles):
|
||||
assert theItem.itemHandle == theHandle
|
||||
|
||||
# Check that we have the correct archive and trash folders
|
||||
assert theTree.trashRoot() == "a000000000003"
|
||||
assert theTree.archiveRoot() == "a000000000002"
|
||||
assert theTree.isTrashRoot("a000000000003")
|
||||
|
||||
# Try to add another trash folder
|
||||
itemT = NWItem(theProject)
|
||||
itemT.itemName = "Trash"
|
||||
itemT.itemType = nwItemType.TRASH
|
||||
itemT.itemClass = nwItemClass.TRASH
|
||||
itemT.isExpanded = False
|
||||
|
||||
assert not theTree.append("1234567890abc", None, itemT)
|
||||
assert len(theTree) == len(dummyItems)
|
||||
|
||||
# Generate handle automatically
|
||||
itemT = NWItem(theProject)
|
||||
itemT.itemName = "New File"
|
||||
itemT.itemType = nwItemType.FILE
|
||||
itemT.itemClass = nwItemClass.NOVEL
|
||||
itemT.itemLayout = nwItemLayout.SCENE
|
||||
|
||||
assert theTree.append(None, None, itemT)
|
||||
assert len(theTree) == len(dummyItems) + 1
|
||||
|
||||
theList = theTree.handles()
|
||||
assert theList[-1] == "73475cb40a568"
|
||||
|
||||
# Try to add existing handle
|
||||
assert not theTree.append("73475cb40a568", None, itemT)
|
||||
assert len(theTree) == len(dummyItems) + 1
|
||||
|
||||
# Delete a non-existing item
|
||||
del theTree["dummy"]
|
||||
assert len(theTree) == len(dummyItems) + 1
|
||||
|
||||
# Delete the last item
|
||||
del theTree["73475cb40a568"]
|
||||
assert len(theTree) == len(dummyItems)
|
||||
assert "73475cb40a568" not in theTree
|
||||
|
||||
# Delete the Novel, Archive and Trash folders
|
||||
del theTree["a000000000001"]
|
||||
assert len(theTree) == len(dummyItems) - 1
|
||||
assert "a000000000001" not in theTree
|
||||
|
||||
del theTree["a000000000002"]
|
||||
assert len(theTree) == len(dummyItems) - 2
|
||||
assert "a000000000002" not in theTree
|
||||
assert theTree.archiveRoot() is None
|
||||
|
||||
del theTree["a000000000003"]
|
||||
assert len(theTree) == len(dummyItems) - 3
|
||||
assert "a000000000003" not in theTree
|
||||
assert theTree.trashRoot() is None
|
||||
|
||||
# END Test testCoreTree_BuildTree
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_Methods(dummyGUI, dummyItems):
|
||||
"""Test building a project tree from a list of items.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHande, nwItem in dummyItems:
|
||||
theTree.append(tHandle, pHande, nwItem)
|
||||
|
||||
assert len(theTree) == len(dummyItems)
|
||||
|
||||
# Root item lookup
|
||||
theTree._treeRoots.append("dummy")
|
||||
assert theTree.findRoot(nwItemClass.WORLD) is None
|
||||
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
|
||||
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
|
||||
|
||||
# Check for root uniqueness
|
||||
assert theTree.checkRootUnique(nwItemClass.CUSTOM)
|
||||
assert theTree.checkRootUnique(nwItemClass.WORLD)
|
||||
assert not theTree.checkRootUnique(nwItemClass.NOVEL)
|
||||
assert not theTree.checkRootUnique(nwItemClass.CHARACTER)
|
||||
|
||||
# Find root item of child item
|
||||
assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001"
|
||||
assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001"
|
||||
assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001"
|
||||
assert theTree.getRootItem("dummy") is None
|
||||
|
||||
# Get item path
|
||||
assert theTree.getItemPath("dummy") == []
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
"c000000000001", "b000000000001", "a000000000001"
|
||||
]
|
||||
|
||||
# Break the folder parent handle
|
||||
theTree["b000000000001"].itemParent = "dummy"
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
"c000000000001", "b000000000001"
|
||||
]
|
||||
|
||||
theTree["b000000000001"].itemParent = "a000000000001"
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
"c000000000001", "b000000000001", "a000000000001"
|
||||
]
|
||||
|
||||
# Change file layout
|
||||
assert not theTree.setFileItemLayout("dummy", nwItemLayout.UNNUMBERED)
|
||||
assert not theTree.setFileItemLayout("b000000000001", nwItemLayout.UNNUMBERED)
|
||||
assert not theTree.setFileItemLayout("c000000000001", "stuff")
|
||||
assert theTree.setFileItemLayout("c000000000001", nwItemLayout.UNNUMBERED)
|
||||
assert theTree["c000000000001"].itemLayout == nwItemLayout.UNNUMBERED
|
||||
|
||||
# END Test testCoreTree_Methods
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_MakeHandles(monkeypatch, dummyGUI):
|
||||
"""Test generating item handles.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
theTree.setSeed(42)
|
||||
|
||||
tHandle = theTree._makeHandle()
|
||||
assert tHandle == "73475cb40a568"
|
||||
|
||||
# Add the next in line to the project to foprce duplicate
|
||||
theTree._projTree["44cb730c42048"] = None
|
||||
tHandle = theTree._makeHandle()
|
||||
assert tHandle == "71ee45a3c0db9"
|
||||
|
||||
# Fix the time() function and force a handle collission
|
||||
theTree.setSeed(None)
|
||||
monkeypatch.setattr("nw.core.tree.time", lambda: 123.4)
|
||||
|
||||
tHandle = theTree._makeHandle()
|
||||
theTree._projTree[tHandle] = None
|
||||
assert tHandle == "5f466d7afa48b"
|
||||
|
||||
tHandle = theTree._makeHandle()
|
||||
theTree._projTree[tHandle] = None
|
||||
assert tHandle == "a79acf4c634a7"
|
||||
|
||||
monkeypatch.undo()
|
||||
|
||||
# END Test testCoreTree_MakeHandles
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_Stats(dummyGUI, dummyItems):
|
||||
"""Test project stats methods.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHande, nwItem in dummyItems:
|
||||
theTree.append(tHandle, pHande, nwItem)
|
||||
|
||||
assert len(theTree) == len(dummyItems)
|
||||
theTree._treeOrder.append("dummy")
|
||||
|
||||
# Count Words
|
||||
novelWords, noteWords = theTree.sumWords()
|
||||
assert novelWords == 550
|
||||
assert noteWords == 400
|
||||
|
||||
# Count types
|
||||
nRoot, nFolder, nFile = theTree.countTypes()
|
||||
assert nRoot == 3
|
||||
assert nFolder == 1
|
||||
assert nFile == 3
|
||||
|
||||
# END Test testCoreTree_Stats
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_Reorder(dummyGUI, dummyItems):
|
||||
"""Test changing tree order.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
aHandle = []
|
||||
for tHandle, pHande, nwItem in dummyItems:
|
||||
aHandle.append(tHandle)
|
||||
theTree.append(tHandle, pHande, nwItem)
|
||||
|
||||
assert len(theTree) == len(dummyItems)
|
||||
|
||||
bHandle = aHandle.copy()
|
||||
bHandle[2], bHandle[3] = bHandle[3], bHandle[2]
|
||||
assert aHandle != bHandle
|
||||
|
||||
assert theTree.handles() == aHandle
|
||||
theTree.setOrder(bHandle)
|
||||
assert theTree.handles() == bHandle
|
||||
|
||||
theTree.setOrder(bHandle + ["dummy"])
|
||||
assert theTree.handles() == bHandle
|
||||
|
||||
theTree._treeOrder.append("dummy")
|
||||
theTree.setOrder(bHandle)
|
||||
assert theTree.handles() == bHandle
|
||||
|
||||
# END Test testCoreTree_Reorder
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems):
|
||||
"""Test changing tree order.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHande, nwItem in dummyItems:
|
||||
theTree.append(tHandle, pHande, nwItem)
|
||||
|
||||
assert len(theTree) == len(dummyItems)
|
||||
|
||||
nwXML = etree.Element("novelWriterXML")
|
||||
theTree.packXML(nwXML)
|
||||
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
|
||||
b"<novelWriterXML>"
|
||||
b"<content count=\"8\">"
|
||||
b"<item handle=\"a000000000001\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Novel</name><type>ROOT</type><class>NOVEL</class><status>None</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"<item handle=\"b000000000001\" order=\"0\" parent=\"a000000000001\">"
|
||||
b"<name>Act One</name><type>FOLDER</type><class>NOVEL</class><status>None</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"<item handle=\"c000000000001\" order=\"0\" parent=\"b000000000001\">"
|
||||
b"<name>Chapter One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
|
||||
b"<exported>True</exported><layout>CHAPTER</layout><charCount>300</charCount>"
|
||||
b"<wordCount>50</wordCount><paraCount>2</paraCount><cursorPos>0</cursorPos></item>"
|
||||
b"<item handle=\"c000000000002\" order=\"0\" parent=\"b000000000001\">"
|
||||
b"<name>Scene One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
|
||||
b"<exported>True</exported><layout>SCENE</layout><charCount>3000</charCount>"
|
||||
b"<wordCount>500</wordCount><paraCount>20</paraCount><cursorPos>0</cursorPos></item>"
|
||||
b"<item handle=\"a000000000002\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Outtakes</name><type>ROOT</type><class>ARCHIVE</class><status>None</status>"
|
||||
b"<expanded>False</expanded></item>"
|
||||
b"<item handle=\"a000000000003\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Trash</name><type>TRASH</type><class>TRASH</class><status>None</status>"
|
||||
b"<expanded>False</expanded></item>"
|
||||
b"<item handle=\"a000000000004\" order=\"0\" parent=\"None\">"
|
||||
b"<name>Characters</name><type>ROOT</type><class>CHARACTER</class><status>None</status>"
|
||||
b"<expanded>True</expanded></item>"
|
||||
b"<item handle=\"b000000000002\" order=\"0\" parent=\"a000000000002\">"
|
||||
b"<name>Jane Doe</name><type>FILE</type><class>CHARACTER</class><status>None</status>"
|
||||
b"<exported>True</exported><layout>NOTE</layout><charCount>2000</charCount>"
|
||||
b"<wordCount>400</wordCount><paraCount>16</paraCount><cursorPos>0</cursorPos></item>"
|
||||
b"</content></novelWriterXML>"
|
||||
)
|
||||
|
||||
theTree.clear()
|
||||
assert len(theTree) == 0
|
||||
assert not theTree.unpackXML(nwXML)
|
||||
assert theTree.unpackXML(nwXML[0])
|
||||
assert len(theTree) == len(dummyItems)
|
||||
|
||||
# END Test testCoreTree_XMLPackUnpack
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir):
|
||||
"""Test writing the ToC.txt file.
|
||||
"""
|
||||
theProject = NWProject(dummyGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHande, nwItem in dummyItems:
|
||||
theTree.append(tHandle, pHande, nwItem)
|
||||
|
||||
assert len(theTree) == len(dummyItems)
|
||||
theTree._treeOrder.append("dummy")
|
||||
|
||||
def dummyIsFile(fileName):
|
||||
"""Return True for items that are files in novelWriter and
|
||||
should thus also be files in the project folder structure.
|
||||
"""
|
||||
dItem = theTree[fileName[8:21]]
|
||||
assert dItem is not None
|
||||
return dItem.itemType == nwItemType.FILE
|
||||
|
||||
monkeypatch.setattr("os.path.isfile", dummyIsFile)
|
||||
|
||||
theProject.projContent = "content"
|
||||
theProject.projPath = None
|
||||
assert not theTree.writeToCFile()
|
||||
|
||||
theProject.projPath = tmpDir
|
||||
assert theTree.writeToCFile()
|
||||
|
||||
pathA = os.path.join("content", "c000000000001.nwd")
|
||||
pathB = os.path.join("content", "c000000000002.nwd")
|
||||
pathC = os.path.join("content", "b000000000002.nwd")
|
||||
|
||||
with open(os.path.join(tmpDir, nwFiles.TOC_TXT), mode="r", encoding="utf8") as inFile:
|
||||
assert inFile.read() == (
|
||||
"\n"
|
||||
"Table of Contents\n"
|
||||
"=================\n"
|
||||
"\n"
|
||||
"File Name Class Layout Document Label\n"
|
||||
"-------------------------------------------------------------\n"
|
||||
f"{pathA} NOVEL CHAPTER Chapter One\n"
|
||||
f"{pathB} NOVEL SCENE Scene One\n"
|
||||
f"{pathC} CHARACTER NOTE Jane Doe\n"
|
||||
)
|
||||
|
||||
# END Test testCoreTree_ToCFile
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Error Tester
|
||||
"""
|
||||
|
||||
import nw
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
from nw.error import NWErrorMessage, exceptionHandler
|
||||
|
||||
@pytest.mark.error
|
||||
def testErrorDialog(qtbot, nwFuncTemp, nwTemp):
|
||||
qApp.closeAllWindows()
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
|
||||
nwErr = NWErrorMessage(nwGUI)
|
||||
qtbot.addWidget(nwErr)
|
||||
nwErr.show()
|
||||
|
||||
# Invalid Error
|
||||
nwErr.setMessage(Exception, "Faulty Error", 123)
|
||||
assert nwErr.msgBody.toPlainText() == "Failed to generate error report ..."
|
||||
|
||||
# Valid Error
|
||||
nwErr.setMessage(Exception, "First Error", None)
|
||||
theMessage = nwErr.msgBody.toPlainText()
|
||||
assert theMessage
|
||||
assert "First Error" in theMessage
|
||||
assert "Exception" in theMessage
|
||||
nwErr._doClose()
|
||||
nwErr.close()
|
||||
|
||||
theMessage = exceptionHandler(Exception, "Second Error", None, testMode=True)
|
||||
assert theMessage
|
||||
assert "Second Error" in theMessage
|
||||
assert "Exception" in theMessage
|
||||
|
||||
nwGUI.closeMain()
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import sys
|
||||
|
||||
from shutil import copyfile
|
||||
from nwtools import cmpFiles, getGuiItem
|
||||
from tools import cmpFiles, getGuiItem
|
||||
|
||||
from PyQt5.QtCore import Qt, QItemSelectionModel
|
||||
from PyQt5.QtWidgets import (
|
||||
@@ -30,8 +30,8 @@ typeDelay = 1
|
||||
stepDelay = 20
|
||||
|
||||
@pytest.mark.gui
|
||||
def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
||||
def testProjectSettings(qtbot, monkeypatch, yesToAll, fncDir, nwTempGUI, refDir, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -43,8 +43,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
|
||||
|
||||
# Create new project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
||||
nwGUI.mainConf.backupPath = nwFuncTemp
|
||||
assert nwGUI.newProject({"projPath": fncDir})
|
||||
nwGUI.mainConf.backupPath = fncDir
|
||||
|
||||
# Get the dialog object
|
||||
monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None)
|
||||
@@ -135,9 +135,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Check the files
|
||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||
testFile = os.path.join(nwTempGUI, "2_nwProject.nwx")
|
||||
refFile = os.path.join(nwRef, "gui", "2_nwProject.nwx")
|
||||
refFile = os.path.join(refDir, "gui", "2_nwProject.nwx")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 8, 9, 10])
|
||||
|
||||
@@ -145,8 +145,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
||||
def testItemEditor(qtbot, yesToAll, monkeypatch, fncDir, nwTempGUI, refDir, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -154,7 +154,7 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
|
||||
|
||||
# Create new, save, open project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
||||
assert nwGUI.newProject({"projPath": fncDir})
|
||||
assert nwGUI.openDocument("0e17daca5f3e1")
|
||||
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
|
||||
|
||||
@@ -208,9 +208,9 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Check the files
|
||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||
testFile = os.path.join(nwTempGUI, "3_nwProject.nwx")
|
||||
refFile = os.path.join(nwRef, "gui", "3_nwProject.nwx")
|
||||
refFile = os.path.join(refDir, "gui", "3_nwProject.nwx")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
|
||||
@@ -218,8 +218,8 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
||||
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, fncDir, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -227,13 +227,13 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
|
||||
# Create new, save, close project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
||||
assert nwGUI.newProject({"projPath": fncDir})
|
||||
qtbot.wait(200)
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeProject()
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
sessFile = os.path.join(nwFuncTemp, "meta", nwFiles.SESS_STATS)
|
||||
sessFile = os.path.join(fncDir, "meta", nwFiles.SESS_STATS)
|
||||
with open(sessFile, mode="w+", encoding="utf-8") as outFile:
|
||||
outFile.write(
|
||||
"# Start Time End Time Novel Notes\n"
|
||||
@@ -244,10 +244,10 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
)
|
||||
|
||||
# Open again, and check the stats
|
||||
assert nwGUI.openProject(nwFuncTemp)
|
||||
assert nwGUI.openProject(fncDir)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
nwGUI.mainConf.lastPath = nwFuncTemp
|
||||
nwGUI.mainConf.lastPath = fncDir
|
||||
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
|
||||
qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000)
|
||||
|
||||
@@ -264,7 +264,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(100)
|
||||
|
||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
||||
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.load(inFile)
|
||||
|
||||
@@ -282,7 +282,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
||||
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
@@ -299,7 +299,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
||||
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.load(inFile)
|
||||
|
||||
@@ -316,7 +316,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
||||
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.load(inFile)
|
||||
|
||||
@@ -329,7 +329,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
||||
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.load(inFile)
|
||||
|
||||
@@ -341,7 +341,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
|
||||
jsonStats = os.path.join(fncDir, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.load(inFile)
|
||||
|
||||
@@ -357,8 +357,8 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
|
||||
def testAboutBox(qtbot, monkeypatch, fncDir, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -374,12 +374,20 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
||||
msgAbout.show()
|
||||
|
||||
assert msgAbout.pageAbout.document().characterCount() > 100
|
||||
assert msgAbout.pageNotes.document().characterCount() > 100
|
||||
assert msgAbout.pageLicense.document().characterCount() > 100
|
||||
|
||||
msgAbout.mainConf.guiLang = "whatever"
|
||||
msgAbout.mainConf.assetPath = "whatever"
|
||||
|
||||
msgAbout._fillNotesPage()
|
||||
assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..."
|
||||
|
||||
msgAbout._fillLicensePage()
|
||||
assert msgAbout.pageLicense.toPlainText() == "Error loading license text ..."
|
||||
|
||||
msgAbout.showReleaseNotes()
|
||||
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes
|
||||
|
||||
# Qt About
|
||||
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None)
|
||||
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
|
||||
@@ -389,9 +397,9 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, refDir, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -420,13 +428,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "build", "1_LoremIpsum.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "build", "1_LoremIpsum.htm")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -459,13 +467,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "build", "2_LoremIpsum.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "build", "2_LoremIpsum.htm")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -479,14 +487,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
assert nwBuild._saveDocument(nwBuild.FMT_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")
|
||||
refFile = os.path.join(refDir, "build", "3_LoremIpsum.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
assert nwBuild._saveDocument(nwBuild.FMT_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")
|
||||
refFile = os.path.join(refDir, "build", "3_LoremIpsum.htm")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -513,14 +521,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
|
||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
|
||||
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.nwd")
|
||||
refFile = os.path.join(nwRef, "build", "4_LoremIpsum.nwd")
|
||||
refFile = os.path.join(refDir, "build", "4_LoremIpsum.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
|
||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
|
||||
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.htm")
|
||||
refFile = os.path.join(nwRef, "build", "4_LoremIpsum.htm")
|
||||
refFile = os.path.join(refDir, "build", "4_LoremIpsum.htm")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -528,14 +536,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
assert nwBuild._saveDocument(nwBuild.FMT_JSON_H)
|
||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
|
||||
testFile = os.path.join(nwTempBuild, "4H_LoremIpsum.json")
|
||||
refFile = os.path.join(nwRef, "build", "4H_LoremIpsum.json")
|
||||
refFile = os.path.join(refDir, "build", "4H_LoremIpsum.json")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [8])
|
||||
|
||||
assert nwBuild._saveDocument(nwBuild.FMT_JSON_M)
|
||||
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
|
||||
testFile = os.path.join(nwTempBuild, "4M_LoremIpsum.json")
|
||||
refFile = os.path.join(nwRef, "build", "4M_LoremIpsum.json")
|
||||
refFile = os.path.join(refDir, "build", "4M_LoremIpsum.json")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [8])
|
||||
|
||||
@@ -581,9 +589,9 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef, nwTemp):
|
||||
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, refDir, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -612,7 +620,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -638,7 +646,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
||||
# This should give us back the file as it was before
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [1, 2, 3])
|
||||
|
||||
@@ -662,19 +670,19 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_31489056e0916.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_98010bd9270f9.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -700,31 +708,31 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [1, 2, 3])
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_031b4af5197ec.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_41cfc0d1f2d12.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_2858dcd1057d3.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
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")
|
||||
refFile = os.path.join(refDir, "gui", "5_2fca346db6561.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -732,7 +740,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
||||
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
# Disable for macOS because the test segfaults on QWizard.show()
|
||||
@@ -744,7 +752,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
||||
ProjWizardCustomPage, ProjWizardFinalPage
|
||||
)
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -929,8 +937,8 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
|
||||
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1009,8 +1017,8 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
|
||||
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir, refDir, tmpConf):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1195,20 +1203,21 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeMain()
|
||||
|
||||
refConf = os.path.join(nwRef, "novelwriter_prefs.conf")
|
||||
refConf = os.path.join(refDir, "novelwriter_prefs.conf")
|
||||
projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf")
|
||||
testConf = os.path.join(nwTemp, "novelwriter_prefs.conf")
|
||||
testConf = os.path.join(tmpDir, "novelwriter_prefs.conf")
|
||||
copyfile(projConf, testConf)
|
||||
ignoreLines = [
|
||||
2, # Timestamp
|
||||
11, 12, 13, 14, 15, 16, 17, # Window sizes
|
||||
7, 27, # Fonts (depends on system default)
|
||||
9, # Release Notes
|
||||
12, 13, 14, 15, 16, 17, 18, # Window sizes
|
||||
7, 28, # Fonts (depends on system default)
|
||||
]
|
||||
assert cmpFiles(testConf, refConf, ignoreLines)
|
||||
|
||||
@pytest.mark.gui
|
||||
def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
||||
def testQuotesDialog(qtbot, yesToAll, nwMinimal, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1237,15 +1246,15 @@ def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
||||
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp)
|
||||
assert nwGUI.selectProjectPath() == nwTemp
|
||||
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: tmpDir)
|
||||
assert nwGUI.selectProjectPath() == tmpDir
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeMain()
|
||||
@@ -4,12 +4,10 @@
|
||||
|
||||
import nw
|
||||
import pytest
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from shutil import copyfile
|
||||
from nwtools import cmpFiles
|
||||
from tools import cmpFiles
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel
|
||||
from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon, QTextBlock
|
||||
@@ -27,85 +25,9 @@ typeDelay = 1
|
||||
stepDelay = 20
|
||||
|
||||
@pytest.mark.gui
|
||||
def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
||||
def testDocEditor(qtbot, yesToAll, fncDir, nwTempGUI, refDir, tmpDir):
|
||||
|
||||
# Defaults
|
||||
nwGUI = nw.main(
|
||||
["--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]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == logging.INFO
|
||||
nwGUI.closeMain()
|
||||
nwGUI.close()
|
||||
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--debug", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == logging.DEBUG
|
||||
nwGUI.closeMain()
|
||||
nwGUI.close()
|
||||
|
||||
nwGUI = nw.main(
|
||||
["--testmode", "--verbose", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
|
||||
)
|
||||
assert nw.logger.getEffectiveLevel() == 5
|
||||
nwGUI.closeMain()
|
||||
nwGUI.close()
|
||||
|
||||
# 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) 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, "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 & 4 == 4 # Python version not satisfied
|
||||
assert ex.value.code & 8 == 8 # Qt version not satisfied
|
||||
assert ex.value.code & 16 == 16 # PyQt version not satisfied
|
||||
assert ex.value.code & 32 == 32 # lxml package missing
|
||||
monkeypatch.undo()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -113,7 +35,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
|
||||
# Create new, save, close project
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
||||
assert nwGUI.newProject({"projPath": fncDir})
|
||||
assert nwGUI.saveProject()
|
||||
assert nwGUI.closeProject()
|
||||
|
||||
@@ -130,9 +52,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
assert not nwGUI.theProject.spellCheck
|
||||
|
||||
# Check the files
|
||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||
testFile = os.path.join(nwTempGUI, "0_nwProject.nwx")
|
||||
refFile = os.path.join(nwRef, "gui", "0_nwProject.nwx")
|
||||
refFile = os.path.join(refDir, "gui", "0_nwProject.nwx")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
qtbot.wait(stepDelay)
|
||||
@@ -140,7 +62,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
# Re-open project
|
||||
assert nwGUI.openProject(nwFuncTemp)
|
||||
assert nwGUI.openProject(fncDir)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
# Check that we loaded the data
|
||||
@@ -148,8 +70,8 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
assert len(nwGUI.theProject.projTree._treeOrder) == 8
|
||||
assert len(nwGUI.theProject.projTree._treeRoots) == 4
|
||||
assert nwGUI.theProject.projTree.trashRoot() is None
|
||||
assert nwGUI.theProject.projPath == nwFuncTemp
|
||||
assert nwGUI.theProject.projMeta == os.path.join(nwFuncTemp, "meta")
|
||||
assert nwGUI.theProject.projPath == fncDir
|
||||
assert nwGUI.theProject.projMeta == os.path.join(fncDir, "meta")
|
||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||
assert nwGUI.theProject.projName == "New Project"
|
||||
assert nwGUI.theProject.bookTitle == ""
|
||||
@@ -379,33 +301,33 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
assert nwGUI.saveProject()
|
||||
|
||||
# Check the files
|
||||
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
|
||||
projFile = os.path.join(fncDir, "nwProject.nwx")
|
||||
testFile = os.path.join(nwTempGUI, "1_nwProject.nwx")
|
||||
refFile = os.path.join(nwRef, "gui", "1_nwProject.nwx")
|
||||
refFile = os.path.join(refDir, "gui", "1_nwProject.nwx")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
|
||||
projFile = os.path.join(nwFuncTemp, "content", "031b4af5197ec.nwd")
|
||||
projFile = os.path.join(fncDir, "content", "031b4af5197ec.nwd")
|
||||
testFile = os.path.join(nwTempGUI, "1_031b4af5197ec.nwd")
|
||||
refFile = os.path.join(nwRef, "gui", "1_031b4af5197ec.nwd")
|
||||
refFile = os.path.join(refDir, "gui", "1_031b4af5197ec.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
projFile = os.path.join(nwFuncTemp, "content", "1a6562590ef19.nwd")
|
||||
projFile = os.path.join(fncDir, "content", "1a6562590ef19.nwd")
|
||||
testFile = os.path.join(nwTempGUI, "1_1a6562590ef19.nwd")
|
||||
refFile = os.path.join(nwRef, "gui", "1_1a6562590ef19.nwd")
|
||||
refFile = os.path.join(refDir, "gui", "1_1a6562590ef19.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
projFile = os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd")
|
||||
projFile = os.path.join(fncDir, "content", "0e17daca5f3e1.nwd")
|
||||
testFile = os.path.join(nwTempGUI, "1_0e17daca5f3e1.nwd")
|
||||
refFile = os.path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd")
|
||||
refFile = os.path.join(refDir, "gui", "1_0e17daca5f3e1.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
projFile = os.path.join(nwFuncTemp, "content", "41cfc0d1f2d12.nwd")
|
||||
projFile = os.path.join(fncDir, "content", "41cfc0d1f2d12.nwd")
|
||||
testFile = os.path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd")
|
||||
refFile = os.path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd")
|
||||
refFile = os.path.join(refDir, "gui", "1_41cfc0d1f2d12.nwd")
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile)
|
||||
|
||||
@@ -414,9 +336,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
def testDocViewer(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -566,9 +488,9 @@ def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp):
|
||||
def testProjectTree(qtbot, yesToAll, nwMinimal, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -690,9 +612,9 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
def testEditFormatMenu(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -927,9 +849,9 @@ def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
def testContextMenu(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1013,15 +935,15 @@ def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
|
||||
def testInsertMenu(qtbot, monkeypatch, fncDir, tmpDir):
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
nwGUI.theProject.projTree.setSeed(42)
|
||||
assert nwGUI.newProject({"projPath": nwFuncTemp})
|
||||
assert nwGUI.newProject({"projPath": fncDir})
|
||||
|
||||
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
|
||||
|
||||
@@ -1162,7 +1084,7 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
|
||||
assert not nwGUI.importDocument()
|
||||
|
||||
# Then a valid path, but bot a file that exists
|
||||
theFile = os.path.join(nwTemp, "import.txt")
|
||||
theFile = os.path.join(tmpDir, "import.txt")
|
||||
monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (theFile, ""))
|
||||
assert not nwGUI.importDocument()
|
||||
|
||||
@@ -1205,16 +1127,16 @@ 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" % os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd")
|
||||
assert theBits[2] == "Location: %s" % os.path.join(fncDir, "content", "0e17daca5f3e1.nwd")
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
nwGUI.closeMain()
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp):
|
||||
def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1392,9 +1314,9 @@ def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testOutline(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
def testOutline(qtbot, yesToAll, nwLipsum, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1453,9 +1375,9 @@ def testOutline(qtbot, yesToAll, nwLipsum, nwTemp):
|
||||
nwGUI.close()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testThemes(qtbot, yesToAll, nwMinimal, nwTemp):
|
||||
def testThemes(qtbot, yesToAll, nwMinimal, tmpDir):
|
||||
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -1478,7 +1400,7 @@ def testThemes(qtbot, yesToAll, nwMinimal, nwTemp):
|
||||
|
||||
# Re-open
|
||||
assert nw.CONFIG.confPath == nwMinimal
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
|
||||
assert nwGUI.mainConf.confPath == nwMinimal
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
@@ -1,679 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Class Tester
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from shutil import copyfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
from nwtools import cmpFiles
|
||||
|
||||
from nw.core.project import NWProject
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple
|
||||
from nw.constants import nwConst, 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 = 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)
|
||||
|
||||
# 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])
|
||||
|
||||
# Open again
|
||||
assert theProject.openProject(projFile)
|
||||
|
||||
# Save and close
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
assert not theProject.projChanged
|
||||
|
||||
# Open a second time
|
||||
assert theProject.openProject(projFile)
|
||||
assert not theProject.openProject(projFile)
|
||||
assert theProject.openProject(projFile, overrideLock=True)
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
||||
"""Check that new root folders can be added to the project.
|
||||
"""
|
||||
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)
|
||||
|
||||
assert theProject.newProject({"projPath": nwFuncTemp})
|
||||
assert theProject.setProjectPath(nwFuncTemp)
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
assert theProject.openProject(projFile)
|
||||
|
||||
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
|
||||
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
|
||||
assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None))
|
||||
assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None))
|
||||
assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
|
||||
assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
|
||||
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
|
||||
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
|
||||
|
||||
assert theProject.projChanged
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
assert not theProject.projChanged
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
||||
"""Check that new files can be added to the project.
|
||||
"""
|
||||
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)
|
||||
|
||||
assert theProject.newProject({"projPath": nwFuncTemp})
|
||||
assert theProject.setProjectPath(nwFuncTemp)
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
assert theProject.openProject(projFile)
|
||||
|
||||
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
|
||||
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
|
||||
assert theProject.projChanged
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
|
||||
assert not theProject.projChanged
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
||||
"""Create a new project from a project wizard dictionary.
|
||||
Custom type with chapters and scenes.
|
||||
"""
|
||||
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",
|
||||
"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": 3,
|
||||
"numScenes": 3,
|
||||
"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 testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy):
|
||||
"""Create a new project from a project wizard dictionary.
|
||||
Custom type without chapters, but with scenes.
|
||||
"""
|
||||
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",
|
||||
"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 testProjectNewSampleA(nwFuncTemp, nwConf, nwDummy, nwTemp):
|
||||
"""Check that we can create a new project can be created from the
|
||||
provided sample project via a zip file.
|
||||
"""
|
||||
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
|
||||
|
||||
# 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 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)
|
||||
|
||||
# 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.
|
||||
"""
|
||||
theProject = NWProject(nwDummy)
|
||||
theProject.projTree.setSeed(42)
|
||||
assert theProject.openProject(nwLipsum)
|
||||
|
||||
aDoc = NWDoc(theProject, nwDummy)
|
||||
assert aDoc.openDocument("47666c91c7ccf")
|
||||
theName, theParent, theClass, theLayout = aDoc.getMeta()
|
||||
|
||||
assert theName == "Scene Five"
|
||||
assert theParent == "6bd935d2490cd"
|
||||
assert theClass == nwItemClass.NOVEL
|
||||
assert theLayout == nwItemLayout.SCENE
|
||||
|
||||
aDoc._docMeta = {"stuff": None}
|
||||
theName, theParent, theClass, theLayout = aDoc.getMeta()
|
||||
assert theName == ""
|
||||
assert theParent is None
|
||||
assert theClass is None
|
||||
assert theLayout is None
|
||||
|
||||
@pytest.mark.project
|
||||
def testSpellEnchant(nwTemp, nwConf):
|
||||
wList = os.path.join(nwTemp, "wordlist.txt")
|
||||
with open(wList, mode="w") as wFile:
|
||||
wFile.write("a_word\nb_word\nc_word\n")
|
||||
|
||||
spChk = NWSpellEnchant()
|
||||
spChk.mainConf = nwConf
|
||||
spChk.setLanguage("en", wList)
|
||||
|
||||
assert spChk.checkWord("a_word")
|
||||
assert spChk.checkWord("b_word")
|
||||
assert spChk.checkWord("c_word")
|
||||
assert not spChk.checkWord("d_word")
|
||||
|
||||
spChk.addWord("d_word")
|
||||
assert spChk.checkWord("d_word")
|
||||
|
||||
wSuggest = spChk.suggestWords("wrod")
|
||||
assert len(wSuggest) > 0
|
||||
assert "word" in wSuggest
|
||||
|
||||
dList = spChk.listDictionaries()
|
||||
assert len(dList) > 0
|
||||
|
||||
aTag, aName = spChk.describeDict()
|
||||
assert aTag == "en"
|
||||
assert aName != ""
|
||||
|
||||
@pytest.mark.project
|
||||
def testSpellSimple(nwTemp, nwConf):
|
||||
wList = os.path.join(nwTemp, "wordlist.txt")
|
||||
with open(wList, mode="w") as wFile:
|
||||
wFile.write("a_word\nb_word\nc_word\n")
|
||||
|
||||
spChk = NWSpellSimple()
|
||||
spChk.mainConf = nwConf
|
||||
spChk.setLanguage("en", wList)
|
||||
|
||||
assert spChk.checkWord("a_word")
|
||||
assert spChk.checkWord("b_word")
|
||||
assert spChk.checkWord("c_word")
|
||||
assert not spChk.checkWord("d_word")
|
||||
|
||||
spChk.addWord("d_word")
|
||||
assert spChk.checkWord("d_word")
|
||||
|
||||
wSuggest = spChk.suggestWords("wrod")
|
||||
assert len(wSuggest) > 0
|
||||
assert "word" in wSuggest
|
||||
|
||||
dList = spChk.listDictionaries()
|
||||
assert len(dList) > 0
|
||||
|
||||
aTag, aName = spChk.describeDict()
|
||||
assert aTag == "en"
|
||||
assert aName == nwConst.SP_INTERNAL
|
||||
|
||||
@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
|
||||
|
||||
theOpts = theProject.optState
|
||||
assert not theOpts.loadSettings()
|
||||
assert not theOpts.saveSettings()
|
||||
|
||||
# No Settings
|
||||
assert theProject.openProject(nwLipsum)
|
||||
assert theOpts.loadSettings()
|
||||
assert theOpts.saveSettings()
|
||||
assert str(theOpts.theState) == r"{}"
|
||||
|
||||
# Read Invalid Settings and Filter
|
||||
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}}'
|
||||
)
|
||||
assert theOpts.loadSettings()
|
||||
assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 100, 'winHeight': 50}}"
|
||||
|
||||
# Set New Settings
|
||||
assert not theOpts.setValue("NoGroup", "NoName", None)
|
||||
assert not theOpts.setValue("GuiProjectSettings", "NoName", None)
|
||||
assert theOpts.setValue("GuiProjectSettings", "winWidth", 200)
|
||||
assert theOpts.setValue("GuiProjectSettings", "winHeight", 80)
|
||||
assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 200, 'winHeight': 80}}"
|
||||
|
||||
# Check Read/Write Types
|
||||
|
||||
## String
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
|
||||
assert isinstance(theOpts.getString("GuiWritingStats", "winWidth", "456"), str)
|
||||
assert theOpts.getString("GuiWritingStats", "NoName", "456") == "456"
|
||||
|
||||
## Int
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
|
||||
assert isinstance(theOpts.getInt("GuiWritingStats", "winWidth", 456), int)
|
||||
assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
|
||||
assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456
|
||||
|
||||
## Float
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
|
||||
assert isinstance(theOpts.getFloat("GuiWritingStats", "winWidth", 456.0), float)
|
||||
assert theOpts.getFloat("GuiWritingStats", "NoName", 456.0) == 456.0
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
|
||||
assert theOpts.getFloat("GuiWritingStats", "winWidth", 456.0) == 456.0
|
||||
|
||||
## Bool
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", True)
|
||||
assert isinstance(theOpts.getBool("GuiWritingStats", "winWidth", False), bool)
|
||||
assert theOpts.getFloat("GuiWritingStats", "NoName", False) is False
|
||||
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
|
||||
assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False
|
||||
|
||||
@pytest.mark.project
|
||||
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
|
||||
assert theProject.closeProject()
|
||||
|
||||
# First Item with Meta Data
|
||||
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
|
||||
with open(orphPath, mode="w", encoding="utf8") as outFile:
|
||||
outFile.write("%%~name:Mars\n")
|
||||
outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n")
|
||||
outFile.write("%%~kind:WORLD/NOTE\n")
|
||||
outFile.write("%%~invalid\n")
|
||||
outFile.write("\n")
|
||||
|
||||
# Second Item without Meta Data
|
||||
orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd")
|
||||
with open(orphPath, mode="w", encoding="utf8") as outFile:
|
||||
outFile.write("\n")
|
||||
|
||||
# Invalid File Name
|
||||
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt")
|
||||
with open(dummyPath, mode="w", encoding="utf8") as outFile:
|
||||
outFile.write("\n")
|
||||
|
||||
# Invalid File Name
|
||||
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd")
|
||||
with open(dummyPath, mode="w", encoding="utf8") as outFile:
|
||||
outFile.write("\n")
|
||||
|
||||
# Invalid File Name
|
||||
dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd")
|
||||
with open(dummyPath, mode="w", encoding="utf8") as outFile:
|
||||
outFile.write("\n")
|
||||
|
||||
assert theProject.openProject(nwLipsum)
|
||||
assert theProject.projPath is not None
|
||||
assert theProject.projTree["636b6aa9b697bb"] is None
|
||||
assert theProject.projTree["abcdefghijklm"] is None
|
||||
|
||||
# First Item with Meta Data
|
||||
oItem = theProject.projTree["636b6aa9b697b"]
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "Mars"
|
||||
assert oItem.itemHandle == "636b6aa9b697b"
|
||||
assert oItem.itemParent is None
|
||||
assert oItem.itemClass == nwItemClass.WORLD
|
||||
assert oItem.itemType == nwItemType.FILE
|
||||
assert oItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# Second Item without Meta Data
|
||||
oItem = theProject.projTree["736b6aa9b697b"]
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "Orphaned File 1"
|
||||
assert oItem.itemHandle == "736b6aa9b697b"
|
||||
assert oItem.itemParent is None
|
||||
assert oItem.itemClass == nwItemClass.NO_CLASS
|
||||
assert oItem.itemType == nwItemType.FILE
|
||||
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
assert theProject.saveProject(nwLipsum)
|
||||
assert theProject.closeProject()
|
||||
|
||||
@pytest.mark.project
|
||||
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
|
||||
|
||||
# Create dummy files for known legacy files
|
||||
deleteFiles = [
|
||||
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(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
|
||||
os.mkdir(os.path.join(nwOldProj, "stuff"))
|
||||
os.mkdir(os.path.join(nwOldProj, "data_1", "stuff"))
|
||||
|
||||
# Create dummy files
|
||||
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 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 os.path.isfile(aFile)
|
||||
|
||||
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 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 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 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 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 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.txt"))
|
||||
|
||||
@pytest.mark.project
|
||||
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)
|
||||
|
||||
# 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 = os.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 = os.listdir(os.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(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(
|
||||
os.path.join(nwMinimal, "nwProject.nwx"), os.path.join(nwTemp, "extract", "nwProject.nwx")
|
||||
)
|
||||
@@ -70,3 +70,15 @@ def getGuiItem(theName):
|
||||
if qWidget.objectName() == theName:
|
||||
return qWidget
|
||||
return None
|
||||
|
||||
def readFile(fileName):
|
||||
"""Returns the content of a file as a string.
|
||||
"""
|
||||
with open(fileName, mode="r", encoding="utf8") as inFile:
|
||||
return inFile.read()
|
||||
|
||||
def writeFile(fileName, fileData):
|
||||
"""Write the contents of a string to a file.
|
||||
"""
|
||||
with open(fileName, mode="w", encoding="utf8") as outFile:
|
||||
outFile.write(fileData)
|
||||