Merge branch 'main' into merge_patches

This commit is contained in:
Veronica Berglyd Olsen
2022-08-17 23:21:10 +02:00
178 changed files with 17390 additions and 9704 deletions
+27 -4
View File
@@ -36,6 +36,13 @@ from PyQt5.QtWidgets import QMessageBox # noqa: E402
from novelwriter.config import Config # noqa: E402
@pytest.fixture(autouse=True)
def initQt(qtbot):
"""Ensures that the qt main thread is always available in all tests.
"""
return
##
# Core Test Folders
##
@@ -173,6 +180,26 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
return
##
# Python Objects
##
@pytest.fixture(scope="function")
def mockRnd(monkeypatch):
"""Create a mock random number generator that just counts upwards
from 0. This one will generate status/importance flags and handles
in a predictable sequence.
"""
def rnd(n):
for x in range(n):
yield x
gen = rnd(1000)
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
return
##
# Temp Project Folders
##
@@ -241,10 +268,6 @@ def nwOldProj(tmpDir):
return
##
# Useful Fixtures
##
@pytest.fixture(scope="session")
def ipsumText():
"""Return five paragraphs of Lorem Ipsum text.
+76 -213
View File
@@ -1,21 +1,22 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.3" timeStamp="2021-08-02 17:47:39">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:13">
<project>
<name>Lorem Ipsum</name>
<title>Lorem Ipsum</title>
<author>lipsum.com</author>
<saveCount>17</saveCount>
<saveCount>26</saveCount>
<autoCount>24</autoCount>
<editTime>1777</editTime>
<editTime>1863</editTime>
</project>
<settings>
<doBackup>False</doBackup>
<language>en_GB</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>7a992350f3eb6</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>3847</lastWordCount>
<novelWordCount>3109</novelWordCount>
<notesWordCount>738</notesWordCount>
@@ -31,240 +32,102 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="sbaa94f" count="3" red="100" green="100" blue="100">New</entry>
<entry key="s27bf7c" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s92a87b" count="5" red="200" green="150" blue="0">Draft</entry>
<entry key="sedd043" count="7" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i613591" count="6" red="100" green="100" blue="100">New</entry>
<entry key="i560cbf" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i37861c" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="id6b1d0" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="21">
<item handle="b3643d0f92e32" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
<item handle="b3643d0f92e32" parent="None" root="b3643d0f92e32" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Novel</name>
</item>
<item handle="7a992350f3eb6" order="0" parent="b3643d0f92e32">
<name>Lorem Ipsum</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>230</charCount>
<wordCount>40</wordCount>
<paraCount>3</paraCount>
<cursorPos>148</cursorPos>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
</item>
<item handle="8c58a65414c23" order="1" parent="b3643d0f92e32">
<name>Front Matter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>1058</charCount>
<wordCount>176</wordCount>
<paraCount>2</paraCount>
<cursorPos>43</cursorPos>
<item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="sedd043" import="i613591" exported="True">Front Matter</name>
</item>
<item handle="88d59a277361b" order="2" parent="b3643d0f92e32">
<name>Prologue</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Draft</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>584</charCount>
<wordCount>92</wordCount>
<paraCount>1</paraCount>
<cursorPos>4</cursorPos>
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="s92a87b" import="i613591" exported="True">Prologue</name>
</item>
<item handle="db7e733775d4d" order="3" parent="b3643d0f92e32">
<name>Act One</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>35</charCount>
<wordCount>6</wordCount>
<paraCount>1</paraCount>
<cursorPos>42</cursorPos>
<item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="sbaa94f" import="i613591" exported="True">Act One</name>
</item>
<item handle="45e6b01ca35c1" order="4" parent="b3643d0f92e32">
<name>Chapter One</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>Draft</status>
<expanded>True</expanded>
<item handle="45e6b01ca35c1" parent="b3643d0f92e32" root="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter One</name>
</item>
<item handle="fb609cd8319dc" order="0" parent="45e6b01ca35c1">
<name>Chapter One</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Draft</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>419</charCount>
<wordCount>67</wordCount>
<paraCount>1</paraCount>
<cursorPos>56</cursorPos>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter One</name>
</item>
<item handle="88243afbe5ed8" order="1" parent="45e6b01ca35c1">
<name>Scene One</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>2758</charCount>
<wordCount>404</wordCount>
<paraCount>4</paraCount>
<cursorPos>1528</cursorPos>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="sedd043" import="i613591" exported="True">Scene One</name>
</item>
<item handle="f96ec11c6a3da" order="2" parent="45e6b01ca35c1">
<name>Scene Two</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>4043</charCount>
<wordCount>600</wordCount>
<paraCount>6</paraCount>
<cursorPos>2335</cursorPos>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="sedd043" import="i613591" exported="True">Scene Two</name>
</item>
<item handle="846352075de7d" order="5" parent="b3643d0f92e32">
<name>Interlude</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>False</exported>
<layout>DOCUMENT</layout>
<charCount>631</charCount>
<wordCount>109</wordCount>
<paraCount>3</paraCount>
<cursorPos>376</cursorPos>
<item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="sbaa94f" import="i613591" exported="False">Interlude</name>
</item>
<item handle="6bd935d2490cd" order="6" parent="b3643d0f92e32">
<name>Chapter Two</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>Draft</status>
<expanded>True</expanded>
<item handle="6bd935d2490cd" parent="b3643d0f92e32" root="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter Two</name>
</item>
<item handle="441420a886d82" order="0" parent="6bd935d2490cd">
<name>Chapter Two</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Draft</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>477</charCount>
<wordCount>70</wordCount>
<paraCount>1</paraCount>
<cursorPos>56</cursorPos>
<item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
</item>
<item handle="eb103bc70c90c" order="1" parent="6bd935d2490cd">
<name>Scene Three</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>3006</charCount>
<wordCount>439</wordCount>
<paraCount>4</paraCount>
<cursorPos>57</cursorPos>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="sedd043" import="i613591" exported="True">Scene Three</name>
</item>
<item handle="f8c0562e50f1b" order="2" parent="6bd935d2490cd">
<name>Scene Four</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>3839</charCount>
<wordCount>563</wordCount>
<paraCount>6</paraCount>
<cursorPos>56</cursorPos>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="sedd043" import="i613591" exported="True">Scene Four</name>
</item>
<item handle="47666c91c7ccf" order="3" parent="6bd935d2490cd">
<name>Scene Five</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>3644</charCount>
<wordCount>543</wordCount>
<paraCount>5</paraCount>
<cursorPos>351</cursorPos>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="sedd043" import="i613591" exported="True">Scene Five</name>
</item>
<item handle="67a8707f2f249" order="1" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>True</expanded>
<item handle="67a8707f2f249" parent="None" root="67a8707f2f249" order="1" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Characters</name>
</item>
<item handle="4c4f28287af27" order="0" parent="67a8707f2f249">
<name>Mr. Nobody</name>
<type>FILE</type>
<class>CHARACTER</class>
<status>Major</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>1864</charCount>
<wordCount>284</wordCount>
<paraCount>3</paraCount>
<cursorPos>1883</cursorPos>
<item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
</item>
<item handle="6c6afb1247750" order="2" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>True</expanded>
<item handle="6c6afb1247750" parent="None" root="6c6afb1247750" order="2" type="ROOT" class="PLOT">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Plot</name>
</item>
<item handle="2426c6f0ca922" order="0" parent="6c6afb1247750">
<name>Main</name>
<type>FILE</type>
<class>PLOT</class>
<status>Main</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>1369</charCount>
<wordCount>195</wordCount>
<paraCount>2</paraCount>
<cursorPos>1387</cursorPos>
<item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="sbaa94f" import="i613591" exported="True">Main</name>
</item>
<item handle="60bdf227455cc" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>True</expanded>
<item handle="60bdf227455cc" parent="None" root="60bdf227455cc" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">World</name>
</item>
<item handle="04468803b92e1" order="0" parent="60bdf227455cc">
<name>Ancient Europe</name>
<type>FILE</type>
<class>WORLD</class>
<status>Minor</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>1770</charCount>
<wordCount>259</wordCount>
<paraCount>3</paraCount>
<cursorPos>1792</cursorPos>
<item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
</item>
</content>
</novelWriterXML>
+37 -75
View File
@@ -1,22 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.3" timeStamp="2021-08-02 17:47:26">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:21">
<project>
<name>Test Minimal</name>
<title>Minimal</title>
<author>Jane Doe</author>
<author>John Doh</author>
<saveCount>9</saveCount>
<saveCount>17</saveCount>
<autoCount>2</autoCount>
<editTime>113</editTime>
<editTime>150</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<language>en_GB</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>10</lastWordCount>
<novelWordCount>10</novelWordCount>
<notesWordCount>0</notesWordCount>
@@ -29,89 +30,50 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s72322d" count="5" red="100" green="100" blue="100">New</entry>
<entry key="sb6adc7" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s2ae76d" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s541953" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="iffcacb" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i6b130b" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="iece803" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i5ba06e" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="8">
<item handle="a508bb932959c" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
<item handle="a508bb932959c" parent="None" root="a508bb932959c" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="s72322d" import="iffcacb">Novel</name>
</item>
<item handle="a35baf2e93843" order="0" parent="a508bb932959c">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>28</charCount>
<wordCount>6</wordCount>
<paraCount>1</paraCount>
<cursorPos>33</cursorPos>
<item handle="a35baf2e93843" parent="a508bb932959c" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="s72322d" import="iffcacb" exported="True">Title Page</name>
</item>
<item handle="a6d311a93600a" order="1" parent="a508bb932959c">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
<item handle="a6d311a93600a" parent="a508bb932959c" root="a508bb932959c" order="1" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s72322d" import="iffcacb">New Chapter</name>
</item>
<item handle="f5ab3e30151e1" order="0" parent="a6d311a93600a">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>16</cursorPos>
<item handle="f5ab3e30151e1" parent="a6d311a93600a" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
</item>
<item handle="8c659a11cd429" order="1" parent="a6d311a93600a">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>9</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>15</cursorPos>
<item handle="8c659a11cd429" parent="a6d311a93600a" root="a508bb932959c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="s72322d" import="iffcacb" exported="True">New Scene</name>
</item>
<item handle="7695ce551d265" order="1" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="7695ce551d265" parent="None" root="7695ce551d265" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s72322d" import="iffcacb">Plot</name>
</item>
<item handle="afb3043c7b2b3" order="2" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="afb3043c7b2b3" parent="None" root="afb3043c7b2b3" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s72322d" import="iffcacb">Characters</name>
</item>
<item handle="9d5247ab588e0" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="9d5247ab588e0" parent="None" root="9d5247ab588e0" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s72322d" import="iffcacb">World</name>
</item>
</content>
</novelWriterXML>
-1
View File
@@ -29,7 +29,6 @@ class MockGuiMain():
def __init__(self):
self.mainConf = None
self.hasProject = True
self.theIndex = None
self.theProject = None
self.statusBar = MockStatusBar()
+1 -3
View File
@@ -13,8 +13,6 @@ hidehscroll = False
[Sizes]
geometry = 1200, 650
preferences = 700, 615
treecols = 200, 50, 30
novelcols = 200, 50
projcols = 200, 60, 140
mainpane = 300, 800
docpane = 400, 400
@@ -30,7 +28,7 @@ emphlabels = True
[Editor]
textfont = None
textsize = 12
width = 600
width = 700
margin = 40
tabwidth = 40
focuswidth = 800
+121 -95
View File
@@ -1,99 +1,125 @@
{
"tagIndex": {
"Bod": [3, "4c4f28287af27", "CHARACTER", "T000001"],
"Main": [3, "2426c6f0ca922", "PLOT", "T000001"],
"Europe": [3, "04468803b92e1", "WORLD", "T000001"]
},
"refIndex": {
"fb609cd8319dc": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
"tagsIndex": {
"Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"},
"Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"},
"Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"}
},
"88243afbe5ed8": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"f96ec11c6a3da": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"441420a886d82": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"eb103bc70c90c": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"f8c0562e50f1b": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"47666c91c7ccf": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"4c4f28287af27": {
"T000001": [[4, "@plot", "Main"]]
"itemIndex": {
"7a992350f3eb6": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
}
},
"8c58a65414c23": {
"level": "H0",
"headings": {
"T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
}
},
"88d59a277361b": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
}
},
"db7e733775d4d": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
}
},
"fb609cd8319dc": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"88243afbe5ed8": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"f96ec11c6a3da": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"846352075de7d": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
}
},
"441420a886d82": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"eb103bc70c90c": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"f8c0562e50f1b": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"47666c91c7ccf": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"4c4f28287af27": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
"references": {
"T000001": {"Main": "@plot"}
}
},
"2426c6f0ca922": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
}
},
"04468803b92e1": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
}
}
}
},
"fileIndex": {
"7a992350f3eb6": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "layout": "DOCUMENT", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
},
"8c58a65414c23": {
"T000000": {"level": "H0", "title": "", "layout": "DOCUMENT", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
},
"88d59a277361b": {
"T000001": {"level": "H2", "title": "Prologue", "layout": "DOCUMENT", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
},
"db7e733775d4d": {
"T000001": {"level": "H1", "title": "Act One", "layout": "DOCUMENT", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
},
"fb609cd8319dc": {
"T000001": {"level": "H2", "title": "Chapter One", "layout": "DOCUMENT", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
},
"88243afbe5ed8": {
"T000001": {"level": "H3", "title": "Scene One", "layout": "DOCUMENT", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "layout": "DOCUMENT", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
},
"f96ec11c6a3da": {
"T000001": {"level": "H3", "title": "Scene Two", "layout": "DOCUMENT", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "layout": "DOCUMENT", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
},
"846352075de7d": {
"T000001": {"level": "H2", "title": "Why do we use it?", "layout": "DOCUMENT", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
},
"441420a886d82": {
"T000001": {"level": "H2", "title": "Chapter Two", "layout": "DOCUMENT", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
},
"eb103bc70c90c": {
"T000001": {"level": "H3", "title": "Scene Three", "layout": "DOCUMENT", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
},
"f8c0562e50f1b": {
"T000001": {"level": "H3", "title": "Scene Four", "layout": "DOCUMENT", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
},
"47666c91c7ccf": {
"T000001": {"level": "H3", "title": "Scene Five", "layout": "DOCUMENT", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
},
"4c4f28287af27": {
"T000001": {"level": "H1", "title": "Nobody Owens", "layout": "NOTE", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
"2426c6f0ca922": {
"T000001": {"level": "H1", "title": "Main Plot", "layout": "NOTE", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
},
"04468803b92e1": {
"T000001": {"level": "H1", "title": "Ancient Europe", "layout": "NOTE", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
}
},
"fileMeta": {
"7a992350f3eb6": ["H1", 230, 40, 3],
"8c58a65414c23": ["H0", 1058, 176, 2],
"88d59a277361b": ["H2", 584, 92, 1],
"db7e733775d4d": ["H1", 35, 6, 1],
"fb609cd8319dc": ["H2", 419, 67, 1],
"88243afbe5ed8": ["H3", 2758, 404, 4],
"f96ec11c6a3da": ["H3", 4043, 600, 6],
"846352075de7d": ["H2", 631, 109, 3],
"441420a886d82": ["H2", 477, 70, 1],
"eb103bc70c90c": ["H3", 3006, 439, 4],
"f8c0562e50f1b": ["H3", 3839, 563, 6],
"47666c91c7ccf": ["H3", 3644, 543, 5],
"4c4f28287af27": ["H1", 1864, 284, 3],
"2426c6f0ca922": ["H1", 1369, 195, 2],
"04468803b92e1": ["H1", 1770, 259, 3]
}
}
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:30:49">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -14,9 +14,10 @@
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
@@ -29,244 +30,106 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s000008" count="15" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="23">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<content count="22">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 1</name>
</item>
<item handle="811786ad1ae74" order="0" parent="None">
<name>Locations</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.1</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000014" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.2</name>
</item>
<item handle="31489056e0916" order="0" parent="None">
<name>Objects</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000015" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.3</name>
</item>
<item handle="98010bd9270f9" order="0" parent="None">
<name>Entities</name>
<type>ROOT</type>
<class>ENTITY</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 2</name>
</item>
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000017" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.1</name>
</item>
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
<name>Chapter 1</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000018" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.2</name>
</item>
<item handle="031b4af5197ec" order="0" parent="1a6562590ef19">
<name>Chapter 1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000019" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.3</name>
</item>
<item handle="41cfc0d1f2d12" order="0" parent="1a6562590ef19">
<name>Scene 1.1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001a" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 3</name>
</item>
<item handle="2858dcd1057d3" order="0" parent="1a6562590ef19">
<name>Scene 1.2</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001b" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.1</name>
</item>
<item handle="2fca346db6561" order="0" parent="1a6562590ef19">
<name>Scene 1.3</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001c" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.2</name>
</item>
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
<name>Chapter 2</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000001d" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.3</name>
</item>
<item handle="7688b6ef52555" order="0" parent="02d20bbd7e394">
<name>Chapter 2</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="c837649cce43f" order="0" parent="02d20bbd7e394">
<name>Scene 2.1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001f" parent="000000000001e" root="000000000001e" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
</item>
<item handle="6208ef0f7750c" order="0" parent="02d20bbd7e394">
<name>Scene 2.2</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000020" parent="None" root="0000000000020" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="3e1e967e9b793" order="0" parent="02d20bbd7e394">
<name>Scene 2.3</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000021" parent="0000000000020" root="0000000000020" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
</item>
<item handle="39fa9ec190eee" order="0" parent="73475cb40a568">
<name>Chapter 3</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000022" parent="None" root="0000000000022" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="d029fa3a95e17" order="0" parent="39fa9ec190eee">
<name>Chapter 3</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000023" parent="0000000000022" root="0000000000022" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Location</name>
</item>
<item handle="81b8a03f97e87" order="0" parent="39fa9ec190eee">
<name>Scene 3.1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000024" parent="None" root="0000000000024" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Archive</name>
</item>
<item handle="da4ea2a5506f2" order="0" parent="39fa9ec190eee">
<name>Scene 3.2</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="a68b412c42825" order="0" parent="39fa9ec190eee">
<name>Scene 3.3</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000025" parent="None" root="0000000000025" order="0" type="ROOT" class="TRASH">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Trash</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:29:34">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-12 17:45:51">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -14,9 +14,10 @@
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
@@ -29,151 +30,82 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s000008" count="9" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="14">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<content count="16">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1</name>
</item>
<item handle="811786ad1ae74" order="0" parent="None">
<name>Locations</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000013" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3</name>
</item>
<item handle="31489056e0916" order="0" parent="None">
<name>Objects</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 4</name>
</item>
<item handle="98010bd9270f9" order="0" parent="None">
<name>Entities</name>
<type>ROOT</type>
<class>ENTITY</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 5</name>
</item>
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000017" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 6</name>
</item>
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
<name>Scene 1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000018" parent="None" root="0000000000018" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="031b4af5197ec" order="0" parent="73475cb40a568">
<name>Scene 2</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000019" parent="0000000000018" root="0000000000018" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
</item>
<item handle="41cfc0d1f2d12" order="0" parent="73475cb40a568">
<name>Scene 3</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001a" parent="None" root="000000000001a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="2858dcd1057d3" order="0" parent="73475cb40a568">
<name>Scene 4</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001b" parent="000000000001a" root="000000000001a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
</item>
<item handle="2fca346db6561" order="0" parent="73475cb40a568">
<name>Scene 5</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001c" parent="None" root="000000000001c" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
<name>Scene 6</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000001d" parent="000000000001c" root="000000000001c" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Location</name>
</item>
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Archive</name>
</item>
<item handle="000000000001f" parent="None" root="000000000001f" order="0" type="ROOT" class="TRASH">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Trash</name>
</item>
</content>
</novelWriterXML>
@@ -0,0 +1,90 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<project>
<name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>2</lastWordCount>
<novelWordCount>1</novelWordCount>
<notesWordCount>1</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
</titleFormat>
<status>
<entry key="s000008" count="7" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i00000c" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="11">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">World</name>
</item>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">New Chapter</name>
</item>
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item>
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
</item>
<item handle="0000000000028" parent="0000000000015" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Stuff</name>
</item>
<item handle="0000000000029" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="5" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Hello</name>
</item>
<item handle="000000000002a" parent="0000000000012" root="0000000000012" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="4" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Jane</name>
</item>
</content>
</novelWriterXML>
@@ -1,139 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:28:30">
<project>
<name>New Project</name>
<title></title>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>%title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
</importance>
</settings>
<content count="10">
<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="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<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="0" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<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="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="1a6562590ef19" order="0" parent="31489056e0916">
<name>Hello</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="031b4af5197ec" order="0" parent="71ee45a3c0db9">
<name>Jane</name>
<type>FILE</type>
<class>CHARACTER</class>
<status>New</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
</content>
</novelWriterXML>
@@ -1,9 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:31:23">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<project>
<name>New Project</name>
<title></title>
<saveCount>1</saveCount>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
@@ -12,9 +12,10 @@
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
@@ -27,89 +28,50 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s000008" count="5" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i00000c" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item>
<item handle="811786ad1ae74" order="0" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000014" parent="None" root="0000000000014" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="31489056e0916" order="0" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000015" parent="None" root="0000000000015" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000016" parent="None" root="0000000000016" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000017" parent="None" root="0000000000017" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Archive</name>
</item>
</content>
</novelWriterXML>
@@ -1,8 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:29:01">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<project>
<name>New Project</name>
<title></title>
<title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
@@ -12,9 +13,10 @@
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
@@ -27,117 +29,82 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s000008" count="6" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i00000c" count="10" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="12">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<content count="16">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="811786ad1ae74" order="0" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">World</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" order="0" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">New Chapter</name>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
</item>
<item handle="1a6562590ef19" order="0" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000028" parent="None" root="0000000000028" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="031b4af5197ec" order="0" parent="None">
<name>Object</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000029" parent="None" root="0000000000029" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="41cfc0d1f2d12" order="0" parent="None">
<name>Custom1</name>
<type>ROOT</type>
<class>CUSTOM</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000002a" parent="None" root="000000000002a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="2858dcd1057d3" order="0" parent="None">
<name>Custom2</name>
<type>ROOT</type>
<class>CUSTOM</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000002b" parent="None" root="000000000002b" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="000000000002c" parent="None" root="000000000002c" order="0" type="ROOT" class="TIMELINE">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Timeline</name>
</item>
<item handle="000000000002d" parent="None" root="000000000002d" order="0" type="ROOT" class="OBJECT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Objects</name>
</item>
<item handle="000000000002e" parent="None" root="000000000002e" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Custom</name>
</item>
<item handle="000000000002f" parent="None" root="000000000002f" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Custom</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
%%~name: New Scene
%%~path: 31489056e0916/0e17daca5f3e1
%%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT
# Novel
@@ -1,5 +1,5 @@
%%~name: New File
%%~path: 71ee45a3c0db9/1a6562590ef19
%%~name: New Note
%%~path: 000000000000a/0000000000020
%%~kind: CHARACTER/NOTE
# Jane Doe
@@ -1,5 +1,5 @@
%%~name: New File
%%~path: 44cb730c42048/031b4af5197ec
%%~name: New Note
%%~path: 0000000000009/0000000000021
%%~kind: PLOT/NOTE
# Main Plot
@@ -1,5 +1,5 @@
%%~name: New File
%%~path: 811786ad1ae74/41cfc0d1f2d12
%%~name: New Note
%%~path: 000000000000b/0000000000022
%%~kind: WORLD/NOTE
# Main Location
@@ -1,22 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-beta2" hexVersion="0x010500b2" fileVersion="1.3" timeStamp="2021-08-31 00:03:45">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:44:39">
<project>
<name>New Project</name>
<title></title>
<saveCount>5</saveCount>
<title>New Novel</title>
<author>Jane Doe</author>
<saveCount>4</saveCount>
<autoCount>2</autoCount>
<editTime>4</editTime>
<editTime>3</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>True</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>0e17daca5f3e1</lastEdited>
<lastEdited>000000000000f</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>126</lastWordCount>
<novelWordCount>99</novelWordCount>
<lastNovel>0000000000008</lastNovel>
<lastOutline>0000000000008</lastOutline>
<lastWordCount>129</lastWordCount>
<novelWordCount>102</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/>
<titleFormat>
@@ -27,132 +29,66 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s000000" count="5" red="100" green="100" blue="100">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i000004" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="12">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" order="1" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="1" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>612</charCount>
<wordCount>95</wordCount>
<paraCount>10</paraCount>
<cursorPos>768</cursorPos>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="612" wordCount="95" paraCount="10" cursorPos="768"/>
<name status="s000000" import="i000004" exported="True">New Scene</name>
</item>
<item handle="44cb730c42048" order="1" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>True</expanded>
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
<meta expanded="True"/>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="031b4af5197ec" order="0" parent="44cb730c42048">
<name>New File</name>
<type>FILE</type>
<class>PLOT</class>
<status>New</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>48</charCount>
<wordCount>10</wordCount>
<paraCount>1</paraCount>
<cursorPos>69</cursorPos>
<item handle="0000000000021" parent="0000000000009" root="0000000000009" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="48" wordCount="10" paraCount="1" cursorPos="69"/>
<name status="s000000" import="i000004" exported="True">New Note</name>
</item>
<item handle="71ee45a3c0db9" order="2" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>True</expanded>
<item handle="000000000000a" parent="None" root="000000000000a" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="1a6562590ef19" order="0" parent="71ee45a3c0db9">
<name>New File</name>
<type>FILE</type>
<class>CHARACTER</class>
<status>New</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>34</charCount>
<wordCount>8</wordCount>
<paraCount>1</paraCount>
<cursorPos>51</cursorPos>
<item handle="0000000000020" parent="000000000000a" root="000000000000a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="34" wordCount="8" paraCount="1" cursorPos="51"/>
<name status="s000000" import="i000004" exported="True">New Note</name>
</item>
<item handle="811786ad1ae74" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>True</expanded>
<item handle="000000000000b" parent="None" root="000000000000b" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="s000000" import="i000004">World</name>
</item>
<item handle="41cfc0d1f2d12" order="0" parent="811786ad1ae74">
<name>New File</name>
<type>FILE</type>
<class>WORLD</class>
<status>New</status>
<exported>True</exported>
<layout>NOTE</layout>
<charCount>51</charCount>
<wordCount>9</wordCount>
<paraCount>1</paraCount>
<cursorPos>68</cursorPos>
<item handle="0000000000022" parent="000000000000b" root="000000000000b" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
<name status="s000000" import="i000004" exported="True">New Note</name>
</item>
<item handle="2fca346db6561" order="4" parent="None">
<name>Trash</name>
<type>TRASH</type>
<class>TRASH</class>
<status>None</status>
<expanded>True</expanded>
<item handle="0000000000024" parent="None" root="0000000000024" order="4" type="ROOT" class="TRASH">
<meta expanded="False"/>
<name status="s000000" import="i000004">Trash</name>
</item>
</content>
</novelWriterXML>
@@ -1,9 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 02:49:51">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:34">
<project>
<name>New Project</name>
<title></title>
<saveCount>3</saveCount>
<title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
@@ -12,11 +13,12 @@
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>6</lastWordCount>
<novelWordCount>6</novelWordCount>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>9</lastWordCount>
<novelWordCount>9</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
@@ -27,89 +29,50 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry key="s000000" count="5" red="100" green="100" blue="100">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
<entry key="i000004" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" order="1" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="1" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>9</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">New Scene</name>
</item>
<item handle="44cb730c42048" order="1" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="71ee45a3c0db9" order="2" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000000a" parent="None" root="000000000000a" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="811786ad1ae74" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000000b" parent="None" root="000000000000b" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000000" import="i000004">World</name>
</item>
</content>
</novelWriterXML>
@@ -13,8 +13,6 @@ hidehscroll = True
[Sizes]
geometry = 1200, 650
preferences = 670, 589
treecols = 200, 50, 30
novelcols = 200, 50
projcols = 200, 60, 140
mainpane = 300, 800
docpane = 400, 400
@@ -1,11 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.5-alpha0" hexVersion="0x010500a0" fileVersion="1.2" timeStamp="2021-08-02 03:04:13">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:31">
<project>
<name>Project Name</name>
<title>Project Title</title>
<author>Jane Doe</author>
<author>John Doh</author>
<saveCount>2</saveCount>
<saveCount>1</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
@@ -14,16 +14,17 @@
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>en</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>6</lastWordCount>
<novelWordCount>6</novelWordCount>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>9</lastWordCount>
<novelWordCount>9</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace>
<entry key="A">B</entry>
<entry key="C">D</entry>
<entry key="This">With This Stuff </entry>
<entry key="This">With This Stuff</entry>
</autoReplace>
<titleFormat>
<title>%title%</title>
@@ -33,89 +34,50 @@
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="200" red="50">Finished</entry>
<entry blue="40" green="30" red="20">Final</entry>
<entry key="s000000" count="5" red="100" green="100" blue="100">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
<entry key="s000010" count="0" red="20" green="30" blue="40">Final</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="0" red="0">Final</entry>
<entry key="i000004" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000011" count="0" red="100" green="100" blue="100">Final</entry>
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" order="1" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="1" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>11</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" order="1" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>DOCUMENT</layout>
<charCount>9</charCount>
<wordCount>2</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" exported="True">New Scene</name>
</item>
<item handle="44cb730c42048" order="1" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="71ee45a3c0db9" order="2" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000000a" parent="None" root="000000000000a" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="811786ad1ae74" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
<item handle="000000000000b" parent="None" root="000000000000b" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000000" import="i000004">World</name>
</item>
</content>
</novelWriterXML>
+32 -14
View File
@@ -24,8 +24,6 @@ import os
import time
import pytest
from datetime import datetime
from mock import causeOSError
from tools import writeFile
@@ -33,7 +31,7 @@ from novelwriter.guimain import GuiMain
from novelwriter.common import (
checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle,
isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, checkIntRange,
checkIntTuple, formatInt, formatTimeStamp, formatTime, parseTimeStamp,
minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified,
splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode,
readTextFile, makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
)
@@ -163,6 +161,7 @@ def testBaseCommon_IsItemClass():
assert isItemClass("ARCHIVE") is True
assert isItemClass("TRASH") is True
# Invalid
assert isItemClass("None") is False
assert isItemClass(None) is False
assert isItemClass("STUFF") is False
@@ -178,8 +177,11 @@ def testBaseCommon_IsItemType():
assert isItemType("ROOT") is True
assert isItemType("FOLDER") is True
assert isItemType("FILE") is True
assert isItemType("TRASH") is True
# Deprecated Type
assert isItemType("TRASH") is False
# Invalid
assert isItemType("None") is False
assert isItemType(None) is False
assert isItemType("STUFF") is False
@@ -195,6 +197,16 @@ def testBaseCommon_IsItemLayout():
assert isItemLayout("DOCUMENT") is True
assert isItemLayout("NOTE") is True
# Deprecated Layouts
assert isItemLayout("TITLE") is False
assert isItemLayout("PAGE") is False
assert isItemLayout("BOOK") is False
assert isItemLayout("PARTITION") is False
assert isItemLayout("UNNUMBERED") is False
assert isItemLayout("CHAPTER") is False
assert isItemLayout("SCENE") is False
# Invalid
assert isItemLayout("None") is False
assert isItemLayout(None) is False
assert isItemLayout("STUFF") is False
@@ -228,6 +240,16 @@ def testBaseCommon_CheckIntRange():
# END Test testBaseCommon_CheckIntRange
@pytest.mark.base
def testBaseCommon_MinMax():
"""Test the minmax function.
"""
for i in range(-5, 15):
assert 0 <= minmax(i, 0, 10) <= 10
# END Test testBaseCommon_MinMax
@pytest.mark.base
def testBaseCommon_CheckIntTuple():
"""Test the checkIntTuple function.
@@ -273,18 +295,14 @@ def testBaseCommon_FormatTime():
@pytest.mark.base
def testBaseCommon_ParseTimeStamp():
"""Test the parseTimeStamp function.
def testBaseCommon_Simplified():
"""Test the simplified function.
"""
localEpoch = datetime(2000, 1, 1).timestamp()
assert parseTimeStamp(None, 0.0, allowNone=True) is None
assert parseTimeStamp("None", 0.0, allowNone=True) is None
assert parseTimeStamp("None", 0.0) == 0.0
assert parseTimeStamp("2000-01-01 00:00:00", 123.0) == localEpoch
assert parseTimeStamp("2000-13-01 00:00:00", 123.0) == 123.0
assert parseTimeStamp("2000-01-32 00:00:00", 123.0) == 123.0
assert simplified("Hello World") == "Hello World"
assert simplified(" Hello World ") == "Hello World"
assert simplified("\tHello\n\r\tWorld") == "Hello World"
# END Test testBaseCommon_ParseTimeStamp
# END Test testBaseCommon_Simplified
@pytest.mark.base
+2 -28
View File
@@ -410,32 +410,6 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
assert tmpConf.setPreferencesSize(700, 615)
# 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])
# Novel Tree Columns
tmpConf.guiScale = 2.0
assert tmpConf.setNovelColWidths([10, 20])
assert tmpConf.getNovelColWidths() == [10, 20]
assert tmpConf.novelColWidth == [5, 10]
tmpConf.guiScale = 1.0
assert tmpConf.setNovelColWidths([10, 20])
assert tmpConf.getNovelColWidths() == [10, 20]
assert tmpConf.novelColWidth == [10, 20]
assert tmpConf.setNovelColWidths([200, 50])
# Project Settings Tree Columns
tmpConf.guiScale = 2.0
assert tmpConf.setProjColWidths([10, 20, 30])
@@ -505,13 +479,13 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
# ============
tmpConf.guiScale = 1.0
assert tmpConf.getTextWidth(False) == 600
assert tmpConf.getTextWidth(False) == 700
assert tmpConf.getTextWidth(True) == 800
assert tmpConf.getTextMargin() == 40
assert tmpConf.getTabWidth() == 40
tmpConf.guiScale = 2.0
assert tmpConf.getTextWidth(False) == 1200
assert tmpConf.getTextWidth(False) == 1400
assert tmpConf.getTextWidth(True) == 1600
assert tmpConf.getTextMargin() == 80
assert tmpConf.getTabWidth() == 80
+2 -2
View File
@@ -64,9 +64,9 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
assert theDoc.readDocument() == "### New Scene\n\n"
# Try to open a new (non-existent) file
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
nHandle = theProject.tree.findRoot(nwItemClass.NOVEL)
assert nHandle is not None
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
xHandle = theProject.newFile("New File", nHandle)
theDoc = NWDoc(theProject, xHandle)
assert bool(theDoc) is True
assert repr(theDoc) == f"<NWDoc handle={xHandle}>"
File diff suppressed because it is too large Load Diff
+309 -56
View File
@@ -23,23 +23,30 @@ import pytest
from lxml import etree
from PyQt5.QtGui import QIcon
from novelwriter.core import NWProject
from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@pytest.mark.core
def testCoreItem_Setters(mockGUI):
def testCoreItem_Setters(mockGUI, mockRnd):
"""Test all the simple setters for the NWItem class.
"""
theProject = NWProject(mockGUI)
theItem = NWItem(theProject)
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
importKeys = ["i000004", "i000005", "i000006", "i000007"]
# Name
theItem.setName("A Name")
assert theItem.itemName == "A Name"
theItem.setName("\t A Name ")
assert theItem.itemName == "A Name"
theItem.setName("\t A\t\u2009\u202f\u2002\u2003\u2028\u2029Name ")
assert theItem.itemName == "A Name"
theItem.setName(123)
assert theItem.itemName == ""
@@ -65,6 +72,18 @@ def testCoreItem_Setters(mockGUI):
theItem.setParent("0123456789abc")
assert theItem.itemParent == "0123456789abc"
# Root
theItem.setRoot(None)
assert theItem.itemRoot is None
theItem.setRoot(123)
assert theItem.itemRoot is None
theItem.setRoot("0123456789abcdef")
assert theItem.itemRoot is None
theItem.setRoot("0123456789abg")
assert theItem.itemRoot is None
theItem.setRoot("0123456789abc")
assert theItem.itemRoot == "0123456789abc"
# Order
theItem.setOrder(None)
assert theItem.itemOrder == 0
@@ -74,29 +93,33 @@ def testCoreItem_Setters(mockGUI):
assert theItem.itemOrder == 1
# Importance
theItem.setStatus("Nonsense")
assert theItem.itemStatus == "New"
theItem.setStatus("New")
assert theItem.itemStatus == "New"
theItem.setStatus("Minor")
assert theItem.itemStatus == "Minor"
theItem.setStatus("Major")
assert theItem.itemStatus == "Major"
theItem.setStatus("Main")
assert theItem.itemStatus == "Main"
theItem._class = nwItemClass.CHARACTER
theItem.setImport("Word")
assert theItem.itemImport == importKeys[0] # Default
for key in importKeys:
theItem.setImport(key)
assert theItem.itemImport == key
# Status
theItem._class = 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"
theItem.setStatus("Word")
assert theItem.itemStatus == statusKeys[0] # Default
for key in statusKeys:
theItem.setStatus(key)
assert theItem.itemStatus == key
# Status/Importance Wrapper
theItem._class = nwItemClass.CHARACTER
for key in importKeys:
theItem.setImport(key)
assert theItem.itemImport == key
assert theItem.itemStatus == statusKeys[3] # Should not change
theItem._class = nwItemClass.NOVEL
for key in statusKeys:
theItem.setStatus(key)
assert theItem.itemImport == importKeys[3] # Should not change
assert theItem.itemStatus == key
# Expanded
theItem.setExpanded(8)
@@ -196,6 +219,31 @@ def testCoreItem_Methods(mockGUI):
theItem.setLayout("NOTE")
assert theItem.describeMe() == "Project Note"
# Status + Icon
# =============
theItem.setType("FILE")
theItem.setStatus("Note")
theItem.setImport("Minor")
theItem.setClass("NOVEL")
stT, stI = theItem.getImportStatus()
assert stT == "Note"
assert isinstance(stI, QIcon)
theItem.setImportStatus("Draft")
stT, stI = theItem.getImportStatus()
assert stT == "Draft"
theItem.setClass("CHARACTER")
stT, stI = theItem.getImportStatus()
assert stT == "Minor"
assert isinstance(stI, QIcon)
theItem.setImportStatus("Major")
stT, stI = theItem.getImportStatus()
assert stT == "Major"
# Representation
# ==============
@@ -235,8 +283,8 @@ def testCoreItem_TypeSetter(mockGUI):
assert theItem.itemType == nwItemType.FOLDER
theItem.setType("FILE")
assert theItem.itemType == nwItemType.FILE
theItem.setType("TRASH")
assert theItem.itemType == nwItemType.TRASH
# Alternative
theItem.setType(nwItemType.ROOT)
assert theItem.itemType == nwItemType.ROOT
@@ -256,28 +304,74 @@ def testCoreItem_ClassSetter(mockGUI):
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClass("NONSENSE")
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClass("NO_CLASS")
assert theItem.itemClass == nwItemClass.NO_CLASS
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is True
theItem.setClass("NOVEL")
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.isNovelLike() is True
assert theItem.documentAllowed() is True
assert theItem.isInactive() is False
theItem.setClass("PLOT")
assert theItem.itemClass == nwItemClass.PLOT
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("CHARACTER")
assert theItem.itemClass == nwItemClass.CHARACTER
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("WORLD")
assert theItem.itemClass == nwItemClass.WORLD
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("TIMELINE")
assert theItem.itemClass == nwItemClass.TIMELINE
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("OBJECT")
assert theItem.itemClass == nwItemClass.OBJECT
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("ENTITY")
assert theItem.itemClass == nwItemClass.ENTITY
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("CUSTOM")
assert theItem.itemClass == nwItemClass.CUSTOM
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is False
assert theItem.isInactive() is False
theItem.setClass("ARCHIVE")
assert theItem.itemClass == nwItemClass.ARCHIVE
assert theItem.isNovelLike() is True
assert theItem.documentAllowed() is True
assert theItem.isInactive() is True
theItem.setClass("TRASH")
assert theItem.itemClass == nwItemClass.TRASH
assert theItem.isNovelLike() is False
assert theItem.documentAllowed() is True
assert theItem.isInactive() is True
# Alternative
theItem.setClass(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL
@@ -306,23 +400,7 @@ def testCoreItem_LayoutSetter(mockGUI):
theItem.setLayout("NOTE")
assert theItem.itemLayout == nwItemLayout.NOTE
# Deprecated Layouts
theItem.setLayout("TITLE")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("PAGE")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("BOOK")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("PARTITION")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("UNNUMBERED")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("CHAPTER")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("SCENE")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
# Alternatives
# Alternative
theItem.setLayout(nwItemLayout.NOTE)
assert theItem.itemLayout == nwItemLayout.NOTE
@@ -330,23 +408,83 @@ def testCoreItem_LayoutSetter(mockGUI):
@pytest.mark.core
def testCoreItem_XMLPackUnpack(mockGUI, caplog):
def testCoreItem_ClassDefaults(mockGUI):
"""Test the setter for the default values.
"""
theProject = NWProject(mockGUI)
theItem = NWItem(theProject)
# Root items should not have their class updated
theItem.setParent(None)
theItem.setClass(nwItemClass.NO_CLASS)
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClassDefaults(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NO_CLASS
# Non-root items should have their class updated
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClassDefaults(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL
# Non-layout items should have their layout set based on class
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
theItem.setLayout(nwItemLayout.NO_LAYOUT)
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
theItem.setClassDefaults(nwItemClass.NOVEL)
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
theItem.setLayout(nwItemLayout.NO_LAYOUT)
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
theItem.setClassDefaults(nwItemClass.PLOT)
assert theItem.itemLayout == nwItemLayout.NOTE
# If documents are not allowed in that class, the layout should be changed
theItem.setParent("0123456789abc")
theItem.setClass(nwItemClass.NO_CLASS)
theItem.setLayout(nwItemLayout.DOCUMENT)
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setClassDefaults(nwItemClass.PLOT)
assert theItem.itemLayout == nwItemLayout.NOTE
# In all cases, status and importance should no longer be None
assert theItem.itemStatus is not None
assert theItem.itemImport is not None
# END Test testCoreItem_ClassDefaults
@pytest.mark.core
def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd):
"""Test packing and unpacking XML objects for the NWItem class.
"""
theProject = NWProject(mockGUI)
nwXML = etree.Element("novelWriterXML")
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
importKeys = ["i000004", "i000005", "i000006", "i000007"]
# File
# ====
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
theItem.setRoot("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
theItem.setType("FILE")
theItem.setStatus("Main")
theItem.setImport(importKeys[3])
theItem.setLayout("NOTE")
theItem.setExported(False)
theItem.setParaCount(3)
@@ -358,19 +496,20 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
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>FILE</type><class>NOVEL</class><status>New</status>"
b"<exported>False</exported><layout>NOTE</layout><charCount>7</charCount>"
b"<wordCount>5</wordCount><paraCount>3</paraCount><cursorPos>11</cursorPos></item>"
b"</content>"
)
b'<content>'
b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
b'type="FILE" class="NOVEL" layout="NOTE"><meta expanded="False" charCount="7" '
b'wordCount="5" paraCount="3" cursorPos="11"/><name status="None" import="%s" '
b'exported="False">A Name</name></item>'
b'</content>'
) % bytes(importKeys[3], encoding="utf8")
# Unpack
theItem = NWItem(theProject)
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExported is False
assert theItem.paraCount == 3
@@ -380,6 +519,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FILE
assert theItem.itemLayout == nwItemLayout.NOTE
assert theItem.itemStatus == statusKeys[0] # Was None, should now be default
assert theItem.itemImport == importKeys[3]
# Folder
# ======
@@ -387,11 +528,12 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
theItem.setRoot("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
theItem.setType("FOLDER")
theItem.setStatus("Main")
theItem.setStatus(statusKeys[1])
theItem.setLayout("NOTE")
theItem.setExpanded(True)
theItem.setExported(False)
@@ -404,18 +546,19 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
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>"
)
b'<content>'
b'<item handle="0123456789abc" parent="0123456789abc" root="0123456789abc" order="1" '
b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="%s" '
b'import="None">A Name</name></item>'
b'</content>'
) % bytes(statusKeys[1], encoding="utf8")
# Unpack
theItem = NWItem(theProject)
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExpanded is True
assert theItem.isExported is True
@@ -426,6 +569,8 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FOLDER
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
assert theItem.itemStatus == statusKeys[1]
assert theItem.itemImport == importKeys[0] # Was None, should now be default
# Errors
# ======
@@ -462,3 +607,111 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog):
)
# END Test testCoreItem_XMLPackUnpack
@pytest.mark.core
def testCoreItem_ConvertFromFmt12(mockGUI):
"""Test the setter for all the nwItemLayout values for the NWItem
class using the class names that were present in file format 1.2.
"""
theProject = NWProject(mockGUI)
theItem = NWItem(theProject)
# Deprecated Layouts
theItem.setLayout("TITLE")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("PAGE")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("BOOK")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("PARTITION")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("UNNUMBERED")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("CHAPTER")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("SCENE")
assert theItem.itemLayout == nwItemLayout.DOCUMENT
theItem.setLayout("MUMBOJUMBO")
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
# END Test testCoreItem_ConvertFromFmt12
@pytest.mark.core
def testCoreItem_ConvertFromFmt13(mockGUI):
"""Test packing and unpacking XML objects for the NWItem class from
format version 1.3
"""
theProject = NWProject(mockGUI)
# Make Version 1.3 XML
nwXML = etree.Element("novelWriterXML")
xContent = etree.SubElement(nwXML, "content")
# Folder
xPack = etree.SubElement(xContent, "item", attrib={
"handle": "a000000000001",
"order": "1",
"parent": "b000000000001",
})
NWItem._subPack(xPack, "name", text="Folder")
NWItem._subPack(xPack, "type", text="FOLDER")
NWItem._subPack(xPack, "class", text="NOVEL")
NWItem._subPack(xPack, "status", text="New")
NWItem._subPack(xPack, "expanded", text="True")
# Unpack Folder
theItem = NWItem(theProject)
theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "a000000000001"
assert theItem.itemParent == "b000000000001"
assert theItem.itemOrder == 1
assert theItem.isExpanded is True
assert theItem.isExported is True
assert theItem.charCount == 0
assert theItem.wordCount == 0
assert theItem.paraCount == 0
assert theItem.cursorPos == 0
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FOLDER
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
# File
xPack = etree.SubElement(xContent, "item", attrib={
"handle": "c000000000001",
"order": "2",
"parent": "a000000000001",
})
NWItem._subPack(xPack, "name", text="Scene")
NWItem._subPack(xPack, "type", text="FILE")
NWItem._subPack(xPack, "class", text="NOVEL")
NWItem._subPack(xPack, "status", text="New")
NWItem._subPack(xPack, "exported", text="True")
NWItem._subPack(xPack, "layout", text="DOCUMENT")
NWItem._subPack(xPack, "charCount", text="600")
NWItem._subPack(xPack, "wordCount", text="100")
NWItem._subPack(xPack, "paraCount", text="6")
NWItem._subPack(xPack, "cursorPos", text="50")
# Unpack File
theItem = NWItem(theProject)
theItem.unpackXML(xContent[1])
assert theItem.itemHandle == "c000000000001"
assert theItem.itemParent == "a000000000001"
assert theItem.itemOrder == 2
assert theItem.isExpanded is False
assert theItem.isExported is True
assert theItem.charCount == 600
assert theItem.wordCount == 100
assert theItem.paraCount == 6
assert theItem.cursorPos == 50
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FILE
assert theItem.itemLayout == nwItemLayout.DOCUMENT
# Deprecated Type
theItem.setType("TRASH")
assert theItem.itemType == nwItemType.ROOT
# END Test testCoreItem_ConvertFromFmt13
+268 -217
View File
@@ -19,24 +19,28 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import pytest
from shutil import copyfile
from zipfile import ZipFile
from lxml import etree
from tools import cmpFiles, writeFile, readFile
from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
from mock import causeOSError
from novelwriter.core.project import NWProject
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwFiles
from novelwriter.core.tree import NWTree
from novelwriter.core.index import NWIndex
from novelwriter.core.project import NWProject
from novelwriter.core.options import OptionState
from novelwriter.core.document import NWDoc
@pytest.mark.core
def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd):
"""Create a new project from a project wizard dictionary. With
default setting, creating a Minimal project.
"""
@@ -45,7 +49,6 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx")
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
# Setting no data should fail
assert theProject.newProject({}) is False
@@ -61,10 +64,6 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
# Creating the project once more should fail
assert theProject.newProject({"projPath": fncDir}) is False
# Check the new project
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
# Open again
assert theProject.openProject(projFile) is True
@@ -72,7 +71,7 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
# Open a second time
@@ -82,13 +81,13 @@ def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI):
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
# END Test testCoreProject_NewMinimal
@pytest.mark.core
def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI):
def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd):
"""Create a new project from a project wizard dictionary.
Custom type with chapters and scenes.
"""
@@ -108,29 +107,25 @@ def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI):
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
nwItemClass.TIMELINE,
nwItemClass.OBJECT,
nwItemClass.ENTITY,
],
"addNotes": True,
"numChapters": 3,
"numScenes": 3,
"chFolders": True,
}
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
# END Test testCoreProject_NewCustomA
@pytest.mark.core
def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI):
def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd):
"""Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes.
"""
@@ -150,23 +145,19 @@ def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI):
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
nwItemClass.TIMELINE,
nwItemClass.OBJECT,
nwItemClass.ENTITY,
],
"addNotes": True,
"numChapters": 0,
"numScenes": 6,
"chFolders": True,
}
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
# END Test testCoreProject_NewCustomB
@@ -186,7 +177,6 @@ def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir):
"popCustom": False,
}
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
# Sample set, but no path
assert not theProject.newProject({"popSample": True})
@@ -235,7 +225,6 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir):
"popCustom": False,
}
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
# Make sure we do not pick up the novelwriter/assets/sample.zip file
tmpConf.assetPath = tmpDir
@@ -259,7 +248,7 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir):
@pytest.mark.core
def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
"""Check that new root folders can be added to the project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
@@ -267,62 +256,84 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
buildTestProject(theProject, fncDir)
assert theProject.newProject({"projPath": fncDir}) is True
assert theProject.setProjectPath(fncDir) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
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 isinstance(theProject.newRoot(nwItemClass.NOVEL), str)
assert isinstance(theProject.newRoot(nwItemClass.PLOT), str)
assert isinstance(theProject.newRoot(nwItemClass.CHARACTER), str)
assert isinstance(theProject.newRoot(nwItemClass.WORLD), str)
assert isinstance(theProject.newRoot(nwItemClass.TIMELINE), str)
assert isinstance(theProject.newRoot(nwItemClass.OBJECT), str)
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
assert isinstance(theProject.newRoot(nwItemClass.CUSTOM), str)
assert theProject.projChanged is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
# END Test testCoreProject_NewRoot
@pytest.mark.core
def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI):
def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd):
"""Check that new files can be added to the project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewFileFolder_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx")
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
buildTestProject(theProject, fncDir)
assert theProject.newProject({"projPath": fncDir}) is True
assert theProject.setProjectPath(fncDir) is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
assert theProject.projChanged
# Invalid call
assert theProject.newFolder("New Folder", "1234567890abc") is None
assert theProject.newFile("New File", "1234567890abc") is None
# Add files properly
assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028"
assert theProject.newFile("Hello", "0000000000015") == "0000000000029"
assert theProject.newFile("Jane", "0000000000012") == "000000000002a"
assert "0000000000028" in theProject.tree
assert "0000000000029" in theProject.tree
assert "000000000002a" in theProject.tree
# Write to file, failed
assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle
assert theProject.writeNewFile("0000000000028", 1, True) is False # Not a file
assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content
# Write to file, success
assert theProject.writeNewFile("0000000000029", 2, True) is True
assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n"
assert theProject.writeNewFile("000000000002a", 1, False) is True
assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n"
# Save, close and check
assert theProject.projChanged is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False
# END Test testCoreProject_NewFile
# END Test testCoreProject_NewFileFolder
@pytest.mark.core
@@ -452,12 +463,15 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI):
os.rename(oName, rName)
# Add some legacy stuff that cannot be removed
writeFile(os.path.join(nwMinimal, "junk"), "stuff")
os.mkdir(os.path.join(nwMinimal, "data_0"))
writeFile(os.path.join(nwMinimal, "data_0", "junk"), "stuff")
mockGUI.clear()
assert theProject.openProject(nwMinimal) is True
assert "data_0" in mockGUI.lastAlert
with monkeypatch.context() as mp:
mp.setattr(theProject, "_legacyDataFolder", causeOSError)
os.mkdir(os.path.join(nwMinimal, "data_0"))
writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.nwd"), "stuff")
writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff")
mockGUI.clear()
assert theProject.openProject(nwMinimal) is True
assert "There was an error updating the project." in mockGUI.lastAlert
assert theProject.closeProject()
# END Test testCoreProject_Open
@@ -500,7 +514,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
assert theProject.saveProject() is True
assert theProject.saveCount == saveCount + 1
assert theProject.autoCount == autoCount
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
# Check that a second save creates a .bak file
assert os.path.isfile(backFile) is True
@@ -511,7 +525,7 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir):
assert theProject.saveProject(autoSave=True) is True
assert theProject.saveCount == saveCount
assert theProject.autoCount == autoCount + 1
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
# Close test project
assert theProject.closeProject()
@@ -630,6 +644,11 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
theProject = NWProject(mockGUI)
theProject.openProject(nwMinimal)
# Storage Objects
assert isinstance(theProject.index, NWIndex)
assert isinstance(theProject.tree, NWTree)
assert isinstance(theProject.options, OptionState)
# Move Novel ROOT to after its files
oldOrder = [
"a508bb932959c", # ROOT: Novel
@@ -651,17 +670,17 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
"afb3043c7b2b3", # ROOT: Characters
"9d5247ab588e0", # ROOT: World
]
assert theProject.projTree.handles() == oldOrder
assert theProject.tree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
assert theProject.projTree.handles() == newOrder
assert theProject.tree.handles() == newOrder
# Add a non-existing item
theProject.projTree._treeOrder.append("01234567789abc")
theProject.tree._treeOrder.append("01234567789abc")
# Add an item with a non-existent parent
nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a")
theProject.projTree[nHandle].setParent("cba9876543210")
assert theProject.projTree[nHandle].itemParent == "cba9876543210"
nHandle = theProject.newFile("Test File", "a6d311a93600a")
theProject.tree[nHandle].setParent("cba9876543210")
assert theProject.tree[nHandle].itemParent == "cba9876543210"
retOrder = []
for tItem in theProject.getProjectItems():
@@ -678,19 +697,138 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
"f5ab3e30151e1", # FILE: New Chapter
"8c659a11cd429", # FILE: New Scene
]
assert theProject.projTree[nHandle].itemParent is None
assert theProject.tree[nHandle].itemParent is None
# END Test testCoreProject_AccessItems
@pytest.mark.core
def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
"""Test the status and importance flag handling.
"""
theProject = NWProject(mockGUI)
buildTestProject(theProject, fncDir)
statusKeys = ["s000008", "s000009", "s00000a", "s00000b"]
importKeys = ["i00000c", "i00000d", "i00000e", "i00000f"]
# Change Status
# =============
theProject.tree["0000000000014"].setStatus("Finished")
theProject.tree["0000000000015"].setStatus("Draft")
theProject.tree["0000000000016"].setStatus("Note")
theProject.tree["0000000000017"].setStatus("Finished")
assert theProject.tree["0000000000014"].itemStatus == statusKeys[3]
assert theProject.tree["0000000000015"].itemStatus == statusKeys[2]
assert theProject.tree["0000000000016"].itemStatus == statusKeys[1]
assert theProject.tree["0000000000017"].itemStatus == statusKeys[3]
newList = [
{"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)},
{"key": statusKeys[1], "name": "Draft", "cols": (2, 2, 2)}, # These are swapped
{"key": statusKeys[2], "name": "Note", "cols": (3, 3, 3)}, # These are swapped
{"key": statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed
{"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name
]
assert theProject.setStatusColours(None, None) is False
assert theProject.setStatusColours([], []) is False
assert theProject.setStatusColours(newList, []) is True
assert theProject.statusItems.name(statusKeys[0]) == "New"
assert theProject.statusItems.name(statusKeys[1]) == "Draft"
assert theProject.statusItems.name(statusKeys[2]) == "Note"
assert theProject.statusItems.name(statusKeys[3]) == "Edited"
assert theProject.statusItems.cols(statusKeys[0]) == (1, 1, 1)
assert theProject.statusItems.cols(statusKeys[1]) == (2, 2, 2)
assert theProject.statusItems.cols(statusKeys[2]) == (3, 3, 3)
assert theProject.statusItems.cols(statusKeys[3]) == (4, 4, 4)
# Check the new entry
lastKey = theProject.statusItems.check("Finished")
assert lastKey == "s000018"
assert theProject.statusItems.name(lastKey) == "Finished"
assert theProject.statusItems.cols(lastKey) == (5, 5, 5)
# Delete last entry
assert theProject.setStatusColours([], [lastKey]) is True
assert theProject.statusItems.name(lastKey) == "New"
# Change Importance
# =================
fHandle = theProject.newFile("Jane Doe", "0000000000012")
theProject.tree[fHandle].setImport("Main")
assert theProject.tree[fHandle].itemImport == importKeys[3]
newList = [
{"key": importKeys[0], "name": "New", "cols": (1, 1, 1)},
{"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)},
{"key": importKeys[2], "name": "Major", "cols": (3, 3, 3)},
{"key": importKeys[3], "name": "Min", "cols": (4, 4, 4)},
{"key": None, "name": "Max", "cols": (5, 5, 5)},
]
assert theProject.setImportColours(None, None) is False
assert theProject.setImportColours([], []) is False
assert theProject.setImportColours(newList, []) is True
assert theProject.importItems.name(importKeys[0]) == "New"
assert theProject.importItems.name(importKeys[1]) == "Minor"
assert theProject.importItems.name(importKeys[2]) == "Major"
assert theProject.importItems.name(importKeys[3]) == "Min"
assert theProject.importItems.cols(importKeys[0]) == (1, 1, 1)
assert theProject.importItems.cols(importKeys[1]) == (2, 2, 2)
assert theProject.importItems.cols(importKeys[2]) == (3, 3, 3)
assert theProject.importItems.cols(importKeys[3]) == (4, 4, 4)
# Check the new entry
lastKey = theProject.importItems.check("Max")
assert lastKey == "i00001a"
assert theProject.importItems.name(lastKey) == "Max"
assert theProject.importItems.cols(lastKey) == (5, 5, 5)
# Delete last entry
assert theProject.setImportColours([], [lastKey]) is True
assert theProject.importItems.name(lastKey) == "New"
# Delete Status/Import
# ====================
theProject.statusItems.resetCounts()
for key in list(theProject.statusItems.keys()):
assert theProject.statusItems.remove(key) is True
theProject.importItems.resetCounts()
for key in list(theProject.importItems.keys()):
assert theProject.importItems.remove(key) is True
assert len(theProject.statusItems) == 0
assert len(theProject.importItems) == 0
assert theProject.saveProject() is True
assert theProject.closeProject() is True
# This should restore the default status/import labels
assert theProject.openProject(fncDir) is True
assert theProject.saveProject() is True
assert theProject.statusItems.name("s000023") == "New"
assert theProject.statusItems.name("s000024") == "Note"
assert theProject.statusItems.name("s000025") == "Draft"
assert theProject.statusItems.name("s000026") == "Finished"
assert theProject.importItems.name("i000027") == "New"
assert theProject.importItems.name("i000028") == "Minor"
assert theProject.importItems.name("i000029") == "Major"
assert theProject.importItems.name("i00002a") == "Main"
# END Test testCoreProject_StatusImport
@pytest.mark.core
def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
"""Test other project class methods and functions.
"""
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
assert theProject.projPath == nwMinimal
buildTestProject(theProject, fncDir)
# Setting project path
assert theProject.setProjectPath(None)
@@ -701,16 +839,16 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
assert theProject.projPath == os.path.expanduser("~")
# Create a new folder and populate it
projPath = os.path.join(nwMinimal, "mock1")
projPath = os.path.join(fncDir, "mock1")
assert theProject.setProjectPath(projPath, newProject=True)
# Make os.mkdir fail
monkeypatch.setattr("os.mkdir", causeOSError)
projPath = os.path.join(nwMinimal, "mock2")
projPath = os.path.join(fncDir, "mock2")
assert not theProject.setProjectPath(projPath, newProject=True)
# Set back
assert theProject.setProjectPath(nwMinimal)
assert theProject.setProjectPath(fncDir)
# Project Name
assert theProject.setProjectName(" A Name ")
@@ -748,9 +886,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
# Trash folder
# Should create on first call, and just returned on later calls
assert theProject.projTree["73475cb40a568"] is None
assert theProject.trashFolder() == "73475cb40a568"
assert theProject.trashFolder() == "73475cb40a568"
hTrash = "0000000000018"
assert theProject.tree[hTrash] is None
assert theProject.trashFolder() == hTrash
assert theProject.trashFolder() == hTrash
# Project backup
assert theProject.doBackup is True
@@ -774,9 +913,10 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
# Spell language
theProject.projChanged = False
assert theProject.setSpellLang(None)
assert theProject.projSpell is None
assert theProject.setSpellLang("None")
assert theProject.setSpellLang(None) is False
assert theProject.projSpell is None
assert theProject.setSpellLang("None") is False # Should be interpreded as None
assert theProject.projSpell is None
assert theProject.setSpellLang("en_GB")
assert theProject.projSpell == "en_GB"
@@ -790,11 +930,9 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
assert theProject.setProjectLang("en_GB") is True
assert theProject.projLang == "en_GB"
# Automatic outline update
theProject.projChanged = False
assert theProject.setAutoOutline(True)
assert not theProject.setAutoOutline(False)
assert theProject.projChanged
# Language Lookup
assert theProject.localLookup(1) == "One"
assert theProject.localLookup(10) == "Ten"
# Last edited
theProject.projChanged = False
@@ -816,70 +954,20 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
# Change project tree order
oldOrder = [
"a508bb932959c", "a35baf2e93843", "a6d311a93600a",
"f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
"afb3043c7b2b3", "9d5247ab588e0", "73475cb40a568",
"0000000000010", "0000000000011", "0000000000012",
"0000000000013", "0000000000014", "0000000000015",
"0000000000016", "0000000000017", "0000000000018",
]
newOrder = [
"f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
"a508bb932959c", "a35baf2e93843", "a6d311a93600a",
"afb3043c7b2b3", "9d5247ab588e0",
"0000000000013", "0000000000014", "0000000000015",
"0000000000010", "0000000000011", "0000000000012",
"0000000000016", "0000000000017",
]
assert theProject.projTree.handles() == oldOrder
assert theProject.tree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
assert theProject.projTree.handles() == newOrder
assert theProject.tree.handles() == newOrder
assert theProject.setTreeOrder(oldOrder)
assert theProject.projTree.handles() == oldOrder
# Change status
theProject.projTree["a35baf2e93843"].setStatus("Finished")
theProject.projTree["a6d311a93600a"].setStatus("Draft")
theProject.projTree["f5ab3e30151e1"].setStatus("Note")
theProject.projTree["8c659a11cd429"].setStatus("Finished")
newList = [
("New", 1, 1, 1, "New"),
("Draft", 2, 2, 2, "Note"), # These are swapped
("Note", 3, 3, 3, "Draft"), # These are swapped
("Edited", 4, 4, 4, "Finished"), # Renamed
("Finished", 5, 5, 5, None), # New, with reused name
]
assert theProject.setStatusColours(newList)
assert theProject.statusItems._theLabels == [
"New", "Draft", "Note", "Edited", "Finished"
]
assert theProject.statusItems._theColours == [
(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
]
assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed
assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped
assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped
assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed
# Change importance
fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
theProject.projTree[fHandle].setStatus("Main")
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 theProject.setImportColours(newList)
assert theProject.importItems._theLabels == [
"New", "Minor", "Major", "Min", "Max"
]
assert theProject.importItems._theColours == [
(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
]
assert theProject.projTree[fHandle].itemStatus == "Min"
# Check status counts
assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0]
assert theProject.importItems._theCounts == [0, 0, 0, 0, 0]
theProject.countStatus()
assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0]
assert theProject.importItems._theCounts == [3, 0, 0, 1, 0]
assert theProject.tree.handles() == oldOrder
# Session stats
theProject.currWCount = 200
@@ -894,7 +982,7 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, mockGUI, tmpDir):
assert not theProject._appendSessionStats(idleTime=0)
# Write entry
assert theProject.projMeta == os.path.join(nwMinimal, "meta")
assert theProject.projMeta == os.path.join(fncDir, "meta")
statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS)
theProject.projOpened = 1600002000
@@ -948,9 +1036,17 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
"""
theProject = NWProject(mockGUI)
assert theProject.openProject(nwLipsum)
assert theProject.projTree["636b6aa9b697b"] is None
assert theProject.closeProject()
assert theProject.openProject(nwLipsum) is True
assert theProject.tree["636b6aa9b697b"] is None
# Add a file with non-existent parent
# This file will be renoved from the project on open
oHandle = theProject.newFile("Oops", "b3643d0f92e32")
theProject.tree[oHandle].setParent("1234567890abc")
# Save and close
assert theProject.saveProject() is True
assert theProject.closeProject() is True
# First Item with Meta Data
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
@@ -980,11 +1076,11 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
assert theProject.openProject(nwLipsum)
assert theProject.projPath is not None
assert theProject.projTree["636b6aa9b697bb"] is None
assert theProject.projTree["abcdefghijklm"] is None
assert theProject.tree["636b6aa9b697bb"] is None
assert theProject.tree["abcdefghijklm"] is None
# First Item with Meta Data
oItem = theProject.projTree["636b6aa9b697b"]
oItem = theProject.tree["636b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "[Recovered] Mars"
assert oItem.itemHandle == "636b6aa9b697b"
@@ -994,7 +1090,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
assert oItem.itemLayout == nwItemLayout.NOTE
# Second Item without Meta Data
oItem = theProject.projTree["736b6aa9b697b"]
oItem = theProject.tree["736b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "Recovered File 1"
assert oItem.itemHandle == "736b6aa9b697b"
@@ -1042,14 +1138,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj):
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 mock files
os.mkdir(os.path.join(nwOldProj, "cache"))
for aFile in deleteFiles:
@@ -1063,7 +1151,6 @@ def testCoreProject_OldFormat(mockGUI, 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"))
@@ -1071,12 +1158,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj):
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"))
@@ -1111,10 +1192,6 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
theProject = NWProject(mockGUI)
theProject.setProjectPath(fncDir)
# assert theProject.newProject({"projPath": fncDir})
# assert theProject.saveProject()
# assert theProject.closeProject()
# Check behaviour of deprecated files function on OSError
tstFile = os.path.join(fncDir, "ToC.json")
writeFile(tstFile, "stuff")
@@ -1122,7 +1199,7 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
with monkeypatch.context() as mp:
mp.setattr("os.unlink", causeOSError)
assert not theProject._deprecatedFiles()
assert theProject._deprecatedFiles() is False
assert theProject._deprecatedFiles()
assert not os.path.isfile(tstFile)
@@ -1131,63 +1208,36 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
tstFile = os.path.join(fncDir, "data_0")
writeFile(tstFile, "stuff")
assert os.path.isfile(tstFile)
errList = []
errList = theProject._legacyDataFolder(tstFile, errList)
assert len(errList) > 0
# Move folder in data folder, shouldn't be there
tstData = os.path.join(fncDir, "data_1")
errItem = os.path.join(fncDir, "data_1", "stuff")
os.mkdir(tstData)
os.mkdir(errItem)
assert os.path.isdir(tstData)
assert os.path.isdir(errItem)
# This causes a failure to create the 'junk' folder
with monkeypatch.context() as mp:
mp.setattr("os.mkdir", causeOSError)
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) > 0
# This causes a failure to move 'stuff' to 'junk'
with monkeypatch.context() as mp:
mp.setattr("os.rename", causeOSError)
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) > 0
# This should be successful
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) == 0
assert os.path.isdir(os.path.join(fncDir, "junk", "stuff"))
assert theProject._legacyDataFolder(tstFile) is False
# Check renaming/deleting of old document files
tstData = os.path.join(fncDir, "data_2")
tstDoc1m = os.path.join(tstData, "000000000001_main.nwd")
tstDoc1b = os.path.join(tstData, "000000000001_main.bak")
tstDoc2m = os.path.join(tstData, "000000000002_main.nwd")
tstDoc2b = os.path.join(tstData, "000000000002_main.bak")
tstDoc3m = os.path.join(tstData, "tooshort003_main.nwd")
tstDoc3b = os.path.join(tstData, "tooshort003_main.bak")
tstData2 = os.path.join(fncDir, "data_2")
tstData3 = os.path.join(fncDir, "data_3")
tstDoc1m = os.path.join(tstData2, "000000000001_main.nwd")
tstDoc1b = os.path.join(tstData2, "000000000001_main.bak")
tstDoc2m = os.path.join(tstData2, "000000000002_main.nwd")
tstDoc2b = os.path.join(tstData2, "000000000002_main.bak")
tstDoc3m = os.path.join(tstData3, "tooshort003_main.nwd")
tstDoc3b = os.path.join(tstData3, "tooshort003_main.bak")
tstDir4a = os.path.join(tstData3, "stuff")
os.mkdir(tstData)
os.mkdir(tstData2)
os.mkdir(tstData3)
writeFile(tstDoc1m, "stuff")
writeFile(tstDoc1b, "stuff")
writeFile(tstDoc2m, "stuff")
writeFile(tstDoc2b, "stuff")
writeFile(tstDoc3m, "stuff")
writeFile(tstDoc3b, "stuff")
os.mkdir(tstDir4a)
# Make the above fail
with monkeypatch.context() as mp:
mp.setattr("os.rename", causeOSError)
mp.setattr("os.unlink", causeOSError)
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) > 0
with pytest.raises(OSError):
theProject._legacyDataFolder(tstData2)
theProject._legacyDataFolder(tstData3)
assert os.path.isfile(tstDoc1m)
assert os.path.isfile(tstDoc1b)
assert os.path.isfile(tstDoc2m)
@@ -1196,15 +1246,16 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
assert os.path.isfile(tstDoc3b)
# And succeed ...
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) == 0
assert theProject._legacyDataFolder(tstData2) is True
assert theProject._legacyDataFolder(tstData3) is True
assert not os.path.isdir(tstData)
assert not os.path.isdir(tstData2)
assert os.path.isdir(tstData3)
assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd"))
assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd"))
assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.nwd"))
assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.bak"))
assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.nwd"))
assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.bak"))
assert os.path.isdir(tstDir4a)
# END Test testCoreProject_LegacyData
+297 -78
View File
@@ -20,101 +20,314 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import random
from lxml import etree
from PyQt5.QtGui import QIcon
from novelwriter.core.status import NWStatus
statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"]
importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"]
@pytest.mark.core
def testCoreStatus_Internal():
"""Test all the internal functions of the NWStatus class.
"""
random.seed(42)
theStatus = NWStatus(NWStatus.STATUS)
theImport = NWStatus(NWStatus.IMPORT)
with pytest.raises(Exception):
NWStatus(999)
# Generate Key
# ============
assert theStatus._newKey() == statusKeys[0]
assert theStatus._newKey() == statusKeys[1]
# Key collision, should move to key 3
theStatus.write(statusKeys[2], "Crash", (0, 0, 0))
assert theStatus._newKey() == statusKeys[3]
assert theImport._newKey() == importKeys[0]
assert theImport._newKey() == importKeys[1]
# Key collision, should move to key 3
theImport.write(importKeys[2], "Crash", (0, 0, 0))
assert theImport._newKey() == importKeys[3]
# Check Key
# =========
assert theStatus._isKey(None) is False # Not a string
assert theStatus._isKey("s00000") is False # Too short
assert theStatus._isKey("s000000") is True # Correct length
assert theStatus._isKey("s0000000") is False # Too long
assert theStatus._isKey("i000000") is False # Wrong type
assert theStatus._isKey("q000000") is False # Wrong type
assert theStatus._isKey("s12345H") is False # Not a hex value
assert theStatus._isKey("s12345F") is False # Not a lower case hex value
assert theStatus._isKey("s12345f") is True # Valid hex value
assert theImport._isKey(None) is False # Not a string
assert theImport._isKey("i00000") is False # Too short
assert theImport._isKey("i000000") is True # Correct length
assert theImport._isKey("i0000000") is False # Too long
assert theImport._isKey("s000000") is False # Wrong type
assert theImport._isKey("q000000") is False # Wrong type
assert theImport._isKey("i12345H") is False # Not a hex value
assert theImport._isKey("i12345F") is False # Not a lower case hex value
assert theImport._isKey("i12345f") is True # Valid hex value
# END Test testCoreStatus_Internal
@pytest.mark.core
def testCoreStatus_Iterator():
"""Test the iterator functions of the NWStatus class.
"""
random.seed(42)
theStatus = NWStatus(NWStatus.STATUS)
theStatus.write(None, "New", (100, 100, 100))
theStatus.write(None, "Note", (200, 50, 0))
theStatus.write(None, "Draft", (200, 150, 0))
theStatus.write(None, "Finished", (50, 200, 0))
# Direct access
entry = theStatus[statusKeys[0]]
assert entry["cols"] == (100, 100, 100)
assert entry["name"] == "New"
assert entry["count"] == 0
assert isinstance(entry["icon"], QIcon)
# Iterate
entries = list(theStatus)
assert len(entries) == 4
assert len(theStatus) == 4
# Keys
assert list(theStatus.keys()) == statusKeys
# Items
for index, (key, entry) in enumerate(theStatus.items()):
assert key == statusKeys[index]
assert "cols" in entry
assert "name" in entry
assert "count" in entry
assert "icon" in entry
# Valuse
for entry in theStatus.values():
assert "cols" in entry
assert "name" in entry
assert "count" in entry
assert "icon" in entry
# END Test testCoreStatus_Iterator
@pytest.mark.core
def testCoreStatus_Entries():
"""Test all the simple setters for the NWItem class.
"""Test all the simple setters for the NWStatus class.
"""
theStatus = NWStatus()
random.seed(42)
theStatus = NWStatus(NWStatus.STATUS)
# 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))
# Write
# =====
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
# Have a key
theStatus.write(statusKeys[0], "Entry 1", (200, 100, 50))
assert theStatus[statusKeys[0]]["name"] == "Entry 1"
assert theStatus[statusKeys[0]]["cols"] == (200, 100, 50)
# Lookups
assert theStatus.lookupEntry(None) is None
assert theStatus.lookupEntry("stuff") is None
assert theStatus.lookupEntry("Main") == 3
# Don't have a key
theStatus.write(None, "Entry 2", (210, 110, 60))
assert theStatus[statusKeys[1]]["name"] == "Entry 2"
assert theStatus[statusKeys[1]]["cols"] == (210, 110, 60)
# Checks
assert theStatus.checkEntry(123) == "New"
assert theStatus.checkEntry("Stuff") == "New"
assert theStatus.checkEntry("New ") == "New"
assert theStatus.checkEntry(" Main ") == "Main"
# Wrong colour spec
theStatus.write(None, "Entry 3", "what?")
assert theStatus[statusKeys[2]]["name"] == "Entry 3"
assert theStatus[statusKeys[2]]["cols"] == (100, 100, 100)
# 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"}
# Wrong colour count
theStatus.write(None, "Entry 4", (10, 20))
assert theStatus[statusKeys[3]]["name"] == "Entry 4"
assert theStatus[statusKeys[3]]["cols"] == (100, 100, 100)
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
# Check reverse map
assert theStatus._reverse == {
"Entry 1": statusKeys[0],
"Entry 2": statusKeys[1],
"Entry 3": statusKeys[2],
"Entry 4": statusKeys[3],
}
# Add counts
countTo = [3, 5, 7, 9, 11]
# Check
# =====
# Normal lookup
for key in statusKeys:
assert theStatus.check(key) == key
# Reverse map lookup
assert theStatus.check("Entry 1") == statusKeys[0]
assert theStatus.check("Entry 2") == statusKeys[1]
assert theStatus.check("Entry 3") == statusKeys[2]
assert theStatus.check("Entry 4") == statusKeys[3]
# Non-existing name
assert theStatus.check("Entry 5") == statusKeys[0]
# Name Access
# ===========
assert theStatus.name(statusKeys[0]) == "Entry 1"
assert theStatus.name(statusKeys[1]) == "Entry 2"
assert theStatus.name(statusKeys[2]) == "Entry 3"
assert theStatus.name(statusKeys[3]) == "Entry 4"
assert theStatus.name("blablabla") == "Entry 1"
# Colour Access
# =============
assert theStatus.cols(statusKeys[0]) == (200, 100, 50)
assert theStatus.cols(statusKeys[1]) == (210, 110, 60)
assert theStatus.cols(statusKeys[2]) == (100, 100, 100)
assert theStatus.cols(statusKeys[3]) == (100, 100, 100)
assert theStatus.cols("blablabla") == (200, 100, 50)
# Icon Access
# ===========
assert isinstance(theStatus.icon(statusKeys[0]), QIcon)
assert isinstance(theStatus.icon(statusKeys[1]), QIcon)
assert isinstance(theStatus.icon(statusKeys[2]), QIcon)
assert isinstance(theStatus.icon(statusKeys[3]), QIcon)
assert isinstance(theStatus.icon("blablabla"), QIcon)
# Increment and Count Access
# ==========================
countTo = [3, 5, 7, 9]
for i, n in enumerate(countTo):
for _ in range(n):
theStatus.countEntry(theStatus._theLabels[i])
assert theStatus._theCounts == countTo
theStatus.increment(statusKeys[i])
# 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.count(statusKeys[0]) == countTo[0]
assert theStatus.count(statusKeys[1]) == countTo[1]
assert theStatus.count(statusKeys[2]) == countTo[2]
assert theStatus.count(statusKeys[3]) == countTo[3]
assert theStatus.count("blablabla") == countTo[0]
assert theStatus[9] == (None, None, None)
# Clear counts
theStatus.resetCounts()
assert theStatus._theCounts == [0, 0, 0, 0, 0]
assert theStatus.count(statusKeys[0]) == 0
assert theStatus.count(statusKeys[1]) == 0
assert theStatus.count(statusKeys[2]) == 0
assert theStatus.count(statusKeys[3]) == 0
# Reorder
# =======
cOrder = list(theStatus.keys())
assert cOrder == statusKeys
# Wrong length
assert theStatus.reorder([]) is False
# No change
assert theStatus.reorder(cOrder) is False
# Actual reaorder
nOrder = [
statusKeys[0],
statusKeys[2],
statusKeys[1],
statusKeys[3],
]
assert theStatus.reorder(nOrder) is True
assert list(theStatus.keys()) == nOrder
# Add an unknown key
wOrder = nOrder.copy()
wOrder[3] = theStatus._newKey()
assert theStatus.reorder(wOrder) is False
assert list(theStatus.keys()) == nOrder
# Put it back
assert theStatus.reorder(cOrder) is True
assert list(theStatus.keys()) == cOrder
# Default
# =======
default = theStatus._default
theStatus._default = None
assert theStatus.check("Entry 5") == ""
assert theStatus.name("blablabla") == ""
assert theStatus.cols("blablabla") == (100, 100, 100)
assert theStatus.count("blablabla") == 0
assert isinstance(theStatus.icon("blablabla"), QIcon)
theStatus._default = default
# Remove
# ======
# Non-existing entry
assert theStatus.remove("blablabla") is False
# Non-zero entry
theStatus.increment(statusKeys[3])
assert theStatus.remove(statusKeys[3]) is False
# Delete last entry
theStatus.resetCounts()
lastName = theStatus.name(statusKeys[3])
assert lastName == "Entry 4"
assert theStatus.remove(statusKeys[3]) is True
assert theStatus.check(statusKeys[3]) == theStatus._default
assert theStatus.check(lastName) == theStatus._default
# Delete default entry, Entry 2 is new default
firstName = theStatus.name(theStatus._default)
assert firstName == "Entry 1"
assert theStatus.remove(theStatus._default) is True
assert theStatus.name(firstName) == "Entry 2"
# Remove remaining entries
assert theStatus.remove(statusKeys[1]) is True
assert theStatus.remove(statusKeys[2]) is True
assert len(theStatus) == 0
assert theStatus._default is None
# END Test testCoreStatus_Entries
@pytest.mark.core
def testCoreStatus_XMLPackUnpack():
"""Test all the simple setters for the NWItem class.
"""Test all the XML pack/unpack of the NWStatus 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))
random.seed(42)
theStatus = NWStatus(NWStatus.STATUS)
theStatus.write(None, "New", (100, 100, 100))
theStatus.write(None, "Note", (200, 50, 0))
theStatus.write(None, "Draft", (200, 150, 0))
theStatus.write(None, "Finished", (50, 200, 0))
countTo = [3, 5, 7, 9]
for i, n in enumerate(countTo):
for _ in range(n):
theStatus.countEntry(theStatus._theLabels[i])
theStatus.increment(statusKeys[i])
nwXML = etree.Element("novelWriterXML")
@@ -122,24 +335,30 @@ def testCoreStatus_XMLPackUnpack():
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>"
b'<status>'
b'<entry key="sa3b179" count="3" red="100" green="100" blue="100">New</entry>'
b'<entry key="s1c8031" count="5" red="200" green="50" blue="0">Note</entry>'
b'<entry key="s06671a" count="7" red="200" green="150" blue="0">Draft</entry>'
b'<entry key="sbdd640" count="9" red="50" green="200" blue="0">Finished</entry>'
b'</status>'
)
# Unpack
theStatus = NWStatus()
theStatus = NWStatus(NWStatus.STATUS)
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
assert len(theStatus._store) == 4
assert list(theStatus._store.keys()) == statusKeys
assert theStatus._store[statusKeys[0]]["name"] == "New"
assert theStatus._store[statusKeys[1]]["name"] == "Note"
assert theStatus._store[statusKeys[2]]["name"] == "Draft"
assert theStatus._store[statusKeys[3]]["name"] == "Finished"
assert theStatus._store[statusKeys[0]]["cols"] == (100, 100, 100)
assert theStatus._store[statusKeys[1]]["cols"] == (200, 50, 0)
assert theStatus._store[statusKeys[2]]["cols"] == (200, 150, 0)
assert theStatus._store[statusKeys[3]]["cols"] == (50, 200, 0)
assert theStatus._store[statusKeys[0]]["count"] == countTo[0]
assert theStatus._store[statusKeys[1]]["count"] == countTo[1]
assert theStatus._store[statusKeys[2]]["count"] == countTo[2]
assert theStatus._store[statusKeys[3]]["count"] == countTo[3]
# END Test testCoreStatus_XMLPackUnpack
+1 -4
View File
@@ -24,7 +24,7 @@ import pytest
from tools import readFile
from novelwriter.core import NWProject, NWIndex, ToHtml
from novelwriter.core import NWProject, ToHtml
@pytest.mark.core
@@ -32,7 +32,6 @@ def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
# Novel Files Headers
@@ -235,7 +234,6 @@ def testCoreToHtml_ConvertDirect(mockGUI):
"""Test the converter directly using the ToHtml class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
theHtml._isNovel = True
@@ -606,7 +604,6 @@ def testCoreToHtml_Format(mockGUI):
"""Test all the formatters for the ToHtml class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
# Export Mode
+17 -9
View File
@@ -28,12 +28,17 @@ from novelwriter.core import NWProject, NWDoc
from novelwriter.core.tokenizer import Tokenizer
class BareTokenizer(Tokenizer):
def doConvert(self):
super().doConvert()
@pytest.mark.core
def testCoreToken_Setters(mockGUI):
"""Test all the setters for the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
# Verify defaults
assert theToken._fmtTitle == "%title%"
@@ -131,11 +136,10 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI):
"""Test handling files and text in the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theProject.projTree.setSeed(42)
theProject.projLang = "en"
theProject._loadProjectLocalisation()
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True)
assert theProject.openProject(nwMinimal)
@@ -214,6 +218,10 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI):
"# Notes: Plot\n\n"
)
# Ckeck abstract method
with pytest.raises(NotImplementedError):
theToken.doConvert()
# END Test testCoreToken_TextOps
@@ -222,7 +230,7 @@ def testCoreToken_HeaderFormat(mockGUI):
"""Test the tokenization of header formats in the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True)
# Title
@@ -426,7 +434,7 @@ def testCoreToken_MetaFormat(mockGUI):
"""Test the tokenization of meta formats in the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True)
# Comment
@@ -495,7 +503,7 @@ def testCoreToken_MarginFormat(mockGUI):
"""Test the tokenization of margin formats in the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True)
# Alignment and Indentation
@@ -550,7 +558,7 @@ def testCoreToken_TextFormat(mockGUI):
"""Test the tokenization of text formats in the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True)
# Text
@@ -672,7 +680,7 @@ def testCoreToken_SpecialFormat(mockGUI):
"""Test the tokenization of special formats in the Tokenizer class.
"""
theProject = NWProject(mockGUI)
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
theToken._isNovel = True
@@ -877,7 +885,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
theProject = NWProject(mockGUI)
theProject.projLang = "en"
theProject._loadProjectLocalisation()
theToken = Tokenizer(theProject)
theToken = BareTokenizer(theProject)
# Nothing
theToken._theText = "Some text ...\n"
+1 -4
View File
@@ -24,7 +24,7 @@ import pytest
from tools import readFile
from novelwriter.core import NWProject, NWIndex, ToMarkdown
from novelwriter.core import NWProject, ToMarkdown
@pytest.mark.core
@@ -32,7 +32,6 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
# Headers
@@ -161,7 +160,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
"""Test the converter directly using the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
theMD._isNovel = True
@@ -266,7 +264,6 @@ def testCoreToMarkdown_Format(mockGUI):
"""Test all the formatters for the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
assert theMD._formatKeywords("", theMD.A_NONE) == ""
+1 -8
View File
@@ -28,7 +28,7 @@ from shutil import copyfile
from tools import cmpFiles
from novelwriter.core import NWProject, NWIndex, ToOdt
from novelwriter.core import NWProject, ToOdt
from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag
XML_NS = [
@@ -55,7 +55,6 @@ def testCoreToOdt_Init(mockGUI):
"""Test initialisation of the ODT document.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
# Flat Doc
# ========
@@ -111,7 +110,6 @@ def testCoreToOdt_TextFormatting(mockGUI):
"""Test formatting of paragraphs.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc.initDocument()
@@ -233,7 +231,6 @@ def testCoreToOdt_Convert(mockGUI):
"""Test the converter of the ToOdt class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True
@@ -565,7 +562,6 @@ def testCoreToOdt_ConvertDirect(mockGUI):
otherwise hard to reach conditions.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True
@@ -620,7 +616,6 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True
@@ -657,7 +652,6 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=False)
theDoc._isNovel = True
@@ -737,7 +731,6 @@ def testCoreToOdt_Format(mockGUI):
"""Test the formatters for the ToOdt class.
"""
theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
assert theDoc._formatSynopsis("synopsis text") == (
+123 -97
View File
@@ -21,9 +21,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
import random
from lxml import etree
from hashlib import sha256
from tools import readFile
@@ -33,7 +33,7 @@ from novelwriter.constants import nwFiles
@pytest.fixture(scope="function")
def mockItems(mockGUI):
def mockItems(mockGUI, mockRnd):
"""Create a list of mock items.
"""
theProject = NWProject(mockGUI)
@@ -76,7 +76,7 @@ def mockItems(mockGUI):
itemF = NWItem(theProject)
itemF._name = "Trash"
itemF._type = nwItemType.TRASH
itemF._type = nwItemType.ROOT
itemF._class = nwItemClass.TRASH
itemF._expanded = False
@@ -103,7 +103,7 @@ def mockItems(mockGUI):
("a000000000002", None, itemE),
("a000000000003", None, itemF),
("a000000000004", None, itemG),
("b000000000002", "a000000000002", itemH),
("b000000000002", "a000000000004", itemH),
]
return theItems
@@ -116,26 +116,22 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
theProject = NWProject(mockGUI)
theTree = NWTree(theProject)
theTree.setSeed(42)
assert theTree._handleSeed == 42
# Check that tree is empty (calls NWTree.__bool__)
assert not theTree
assert bool(theTree) is False
# Check for archive and trash folders
assert theTree.trashRoot() is None
assert theTree.archiveRoot() is None
assert not theTree.isTrashRoot("a000000000003")
aHandles = []
for tHandle, pHandle, nwItem in mockItems:
aHandles.append(tHandle)
assert theTree.append(tHandle, pHandle, nwItem)
assert theTree.append(tHandle, pHandle, nwItem) is True
assert theTree.updateItemData(tHandle) is True
assert theTree._treeChanged
assert theTree._treeChanged is True
# Check that tree is not empty (calls __bool__)
assert theTree
assert bool(theTree) is True
# Check the number of elements (calls __len__)
assert len(theTree) == len(mockItems)
@@ -149,17 +145,43 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# Check that we have the correct archive and trash folders
assert theTree.trashRoot() == "a000000000003"
assert theTree.archiveRoot() == "a000000000002"
assert theTree.isTrashRoot("a000000000003")
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
assert theTree.isTrash("a000000000003") is True
assert theTree.isRoot("a000000000002") is True
# Check that we have the root classes
assert theTree.rootClasses() == {
nwItemClass.NOVEL, nwItemClass.CHARACTER, nwItemClass.ARCHIVE, nwItemClass.TRASH
}
# Check the isTrash function
assert theTree.isTrash("0000000000000") is True # Doesn't exist
assert theTree.isTrash("a000000000003") is True # This the trash folder
theTree["a000000000003"].setClass(nwItemClass.NO_CLASS)
assert theTree.isTrash("a000000000003") is True # This is still trash
theTree["a000000000003"].setClass(nwItemClass.TRASH)
assert theTree.isTrash("b000000000002") is False # This is not trash
value = theTree["b000000000002"].itemParent
theTree["b000000000002"].setParent("a000000000003")
assert theTree.isTrash("b000000000002") is True # This is in trash
theTree["b000000000002"].setParent(value)
value = theTree["b000000000002"].itemRoot
theTree["b000000000002"].setRoot("a000000000003")
assert theTree.isTrash("b000000000002") is True # This is in trash
theTree["b000000000002"].setRoot(value)
# Try to add another trash folder
itemT = NWItem(theProject)
itemT._name = "Trash"
itemT._type = nwItemType.TRASH
itemT._type = nwItemType.ROOT
itemT._class = nwItemClass.TRASH
itemT._expanded = False
assert not theTree.append("1234567890abc", None, itemT)
assert theTree.append("1234567890abc", None, itemT) is False
assert len(theTree) == len(mockItems)
# Generate handle automatically
@@ -169,14 +191,16 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.NOVEL
itemT._layout = nwItemLayout.DOCUMENT
assert theTree.append(None, None, itemT)
assert theTree.append(None, None, itemT) is True
assert theTree.updateItemData(itemT.itemHandle) is True
assert len(theTree) == len(mockItems) + 1
theList = theTree.handles()
assert theList[-1] == "73475cb40a568"
nHandle = "0000000000010"
assert theList[-1] == nHandle
# Try to add existing handle
assert not theTree.append("73475cb40a568", None, itemT)
assert theTree.append(nHandle, None, itemT) is False
assert len(theTree) == len(mockItems) + 1
# Delete a non-existing item
@@ -184,9 +208,9 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert len(theTree) == len(mockItems) + 1
# Delete the last item
del theTree["73475cb40a568"]
del theTree[nHandle]
assert len(theTree) == len(mockItems)
assert "73475cb40a568" not in theTree
assert nHandle not in theTree
# Delete the Novel, Archive and Trash folders
del theTree["a000000000001"]
@@ -196,7 +220,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
del theTree["a000000000002"]
assert len(theTree) == len(mockItems) - 2
assert "a000000000002" not in theTree
assert theTree.archiveRoot() is None
del theTree["a000000000003"]
assert len(theTree) == len(mockItems) - 3
@@ -215,31 +238,43 @@ def testCoreTree_Methods(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
# Update item data, nonsense handle
assert theTree.updateItemData("stuff") is False
# Update item data, invalid item parent
corrParent = theTree["b000000000001"].itemParent
theTree["b000000000001"].setParent("0000000000000")
assert theTree.updateItemData("b000000000001") is False
# Update item data, valid item parent
theTree["b000000000001"].setParent(corrParent)
assert theTree.updateItemData("b000000000001") is True
# Update item data, root is unreachable
maxDepth = theTree.MAX_DEPTH
theTree.MAX_DEPTH = 0
with pytest.raises(RecursionError):
theTree.updateItemData("b000000000001")
theTree.MAX_DEPTH = maxDepth
# Chech type
assert theTree.checkType("blabla", nwItemType.FILE) is False
assert theTree.checkType("b000000000001", nwItemType.FILE) is False
assert theTree.checkType("c000000000001", nwItemType.FILE) is True
# Root item lookup
theTree._treeRoots.append("stuff")
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("stuff") is None
# Add a fake item to root and check that it can handle it
theTree._treeRoots["0000000000000"] = NWItem(theProject)
assert theTree.findRoot(nwItemClass.WORLD) is None
del theTree._treeRoots["0000000000000"]
# Get item path
assert theTree.getItemPath("stuff") == []
@@ -247,6 +282,13 @@ def testCoreTree_Methods(mockGUI, mockItems):
"c000000000001", "b000000000001", "a000000000001"
]
# Cause recursion error
maxDepth = theTree.MAX_DEPTH
theTree.MAX_DEPTH = 0
with pytest.raises(RecursionError):
theTree.getItemPath("c000000000001")
theTree.MAX_DEPTH = maxDepth
# Break the folder parent handle
theTree["b000000000001"]._parent = "stuff"
assert theTree.getItemPath("c000000000001") == [
@@ -269,44 +311,31 @@ def testCoreTree_Methods(mockGUI, mockItems):
@pytest.mark.core
def testCoreTree_MakeHandles(monkeypatch, mockGUI):
def testCoreTree_MakeHandles(mockGUI):
"""Test generating item handles.
"""
random.seed(42)
theProject = NWProject(mockGUI)
theTree = NWTree(theProject)
theTree.setSeed(42)
handles = ["1c803a3b1799d", "bdd6406671ad1", "3eb1346685257", "23b8c392456de"]
random.seed(42)
tHandle = theTree._makeHandle()
assert tHandle == "73475cb40a568"
assert tHandle == handles[0]
theTree._projTree[handles[0]] = None
# Add the next in line to the project to force duplicate
theTree._projTree["44cb730c42048"] = None
theTree._projTree[handles[1]] = None
tHandle = theTree._makeHandle()
assert tHandle == "71ee45a3c0db9"
# Fix the time() function and force a handle collission
theTree.setSeed(None)
theTree._handleCount = 0
monkeypatch.setattr("novelwriter.core.tree.time", lambda: 123.4)
assert tHandle == handles[2]
theTree._projTree[handles[2]] = None
# Reset the seed to force collissions, which should still end up
# returning the next handle in the sequence
random.seed(42)
tHandle = theTree._makeHandle()
theTree._projTree[tHandle] = None
newSeed = "123.4_0_"
assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13]
tHandle = theTree._makeHandle()
theTree._projTree[tHandle] = None
newSeed = "123.4_1_"
assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13]
# Reset the count and the handle for 0 and 1 should be duplicates
# which forces the function to add the '!'
theTree._handleCount = 0
tHandle = theTree._makeHandle()
theTree._projTree[tHandle] = None
newSeed = "123.4_1_!"
assert tHandle == sha256(newSeed.encode()).hexdigest()[0:13]
assert tHandle == handles[3]
# END Test testCoreTree_MakeHandles
@@ -329,12 +358,6 @@ def testCoreTree_Stats(mockGUI, mockItems):
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
@@ -379,42 +402,44 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
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>DOCUMENT</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>DOCUMENT</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>"
b'<novelWriterXML>'
b'<content count="8">'
b'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
b'class="NOVEL"><meta expanded="True"/><name status="s000000" '
b'import="i000004">Novel</name></item>'
b'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="s000000" '
b'import="i000004">Act One</name></item>'
b'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta expanded="False" charCount="300" '
b'wordCount="50" paraCount="2" cursorPos="0"/><name status="s000000" import="i000004" '
b'exported="True">Chapter One</name></item>'
b'<item handle="c000000000002" parent="b000000000001" root="a000000000001" order="0" '
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta expanded="False" charCount="3000" '
b'wordCount="500" paraCount="20" cursorPos="0"/><name status="s000000" import="i000004" '
b'exported="True">Scene One</name></item>'
b'<item handle="a000000000002" parent="None" root="a000000000002" order="0" type="ROOT" '
b'class="ARCHIVE"><meta expanded="False"/><name status="s000000" '
b'import="i000004">Outtakes</name></item>'
b'<item handle="a000000000003" parent="None" root="a000000000003" order="0" type="ROOT" '
b'class="TRASH"><meta expanded="False"/><name status="s000000" '
b'import="i000004">Trash</name></item>'
b'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
b'class="CHARACTER"><meta expanded="True"/><name status="s000000" '
b'import="i000004">Characters</name></item>'
b'<item handle="b000000000002" parent="a000000000004" root="a000000000004" order="0" '
b'type="FILE" class="CHARACTER" layout="NOTE"><meta expanded="False" charCount="2000" '
b'wordCount="400" paraCount="16" cursorPos="0"/><name status="s000000" import="i000004" '
b'exported="True">Jane Doe</name></item>'
b'</content>'
b'</novelWriterXML>'
)
theTree.clear()
@@ -435,6 +460,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff")
+2 -2
View File
@@ -36,8 +36,8 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI):
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# NW About
nwGUI.theTheme.themeName = "A Theme"
nwGUI.theTheme.themeAuthor = "An Author"
nwGUI.mainTheme.themeName = "A Theme"
nwGUI.mainTheme.themeAuthor = "An Author"
assert nwGUI.showAboutNWDialog(showNotes=True) is True
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
+22 -5
View File
@@ -24,11 +24,7 @@ import pytest
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QListWidgetItem, QDialog, QMessageBox
from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates
keyDelay = 2
typeDelay = 1
stepDelay = 20
from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates, GuiEditLabel
@pytest.mark.gui
@@ -101,3 +97,24 @@ def testDlgOther_Updates(qtbot, monkeypatch, nwGUI):
nwUpdate._doClose()
# END Test testDlgOther_Updates
@pytest.mark.gui
def testDlgOther_EditLabel(qtbot, monkeypatch):
"""Test the label editor dialog.
"""
monkeypatch.setattr(GuiEditLabel, "exec_", lambda *a: None)
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Accepted)
newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World")
assert dlgOk is True
assert newLabel == "Hello World"
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Rejected)
newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World")
assert dlgOk is False
assert newLabel == "Hello World"
# END Test testDlgOther_EditLabel
+32 -30
View File
@@ -19,48 +19,48 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import pytest
from tools import getGuiItem, readFile, writeFile
from mock import causeOSError
from tools import getGuiItem, readFile, writeFile, buildTestProject
from PyQt5.QtWidgets import QAction, QMessageBox, QDialog
from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.dialogs import GuiDocMerge, GuiItemEditor
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.dialogs import GuiDocMerge, GuiEditLabel
from novelwriter.core.tree import NWTree
@pytest.mark.gui
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test the merge documents tool.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a new project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
buildTestProject(nwGUI, fncProj)
# Handles for new objects
hChapterDir = "31489056e0916"
hChapterOne = "98010bd9270f9"
hSceneOne = "0e17daca5f3e1"
hSceneTwo = "1a6562590ef19"
hSceneThree = "031b4af5197ec"
hSceneFour = "41cfc0d1f2d12"
hMergedDoc = "2858dcd1057d3"
hNovelRoot = "0000000000008"
hChapterDir = "000000000000d"
hChapterOne = "000000000000e"
hSceneOne = "000000000000f"
hSceneTwo = "0000000000010"
hSceneThree = "0000000000011"
hSceneFour = "0000000000012"
hMergedDoc = "0000000000023"
# Add Project Content
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True
assert nwGUI.closeProject() is True
@@ -82,8 +82,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
# Open the Merge tool
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None)
nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger)
@@ -101,27 +101,27 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
assert nwMerge.listBox.count() == 0
# No item selected
nwGUI.treeView.clearSelection()
nwGUI.projView.projTree.clearSelection()
assert nwMerge._populateList() is False
assert nwMerge.listBox.count() == 0
# Non-existing item
with monkeypatch.context() as mp:
mp.setattr(NWTree, "__getitem__", lambda *a: None)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
assert nwMerge._populateList() is False
assert nwMerge.listBox.count() == 0
# Select a non-folder
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterOne).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True)
assert nwMerge._populateList() is False
assert nwMerge.listBox.count() == 0
# Select the chapter folder
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
assert nwMerge._populateList() is True
assert nwMerge.listBox.count() == 5
@@ -137,7 +137,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
assert os.path.isfile(mergedFile)
assert readFile(mergedFile) == (
"%%%%~name: New Chapter\n"
"%%%%~path: 73475cb40a568/2858dcd1057d3\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
"%s\n\n"
@@ -145,6 +145,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
"%s\n\n"
"%s\n\n"
) % (
hNovelRoot,
hMergedDoc,
tChapterOne.strip(),
tSceneOne.strip(),
tSceneTwo.strip(),
+43 -49
View File
@@ -19,50 +19,50 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import pytest
from tools import getGuiItem, readFile, writeFile
from mock import causeOSError
from tools import getGuiItem, readFile, writeFile, buildTestProject
from PyQt5.QtWidgets import QAction, QMessageBox, QDialog
from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.dialogs import GuiDocSplit, GuiItemEditor
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.core.document import NWDoc
from novelwriter.dialogs import GuiDocSplit, GuiEditLabel
from novelwriter.core.tree import NWTree
from novelwriter.core.document import NWDoc
@pytest.mark.gui
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test the split document tool.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a new project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True
buildTestProject(nwGUI, fncProj)
# Handles for new objects
hNovelRoot = "73475cb40a568"
hChapterDir = "31489056e0916"
hToSplit = "1a6562590ef19"
hPartition = "41cfc0d1f2d12"
hChapterOne = "2858dcd1057d3"
hSceneOne = "2fca346db6561"
hSceneTwo = "02d20bbd7e394"
hSceneThree = "7688b6ef52555"
hSceneFour = "c837649cce43f"
hSceneFive = "6208ef0f7750c"
hNovelRoot = "0000000000008"
hChapterDir = "000000000000d"
hToSplit = "0000000000010"
hNewFolder = "0000000000021"
hPartition = "0000000000022"
hChapterOne = "0000000000023"
hSceneOne = "0000000000024"
hSceneTwo = "0000000000025"
hSceneThree = "0000000000026"
hSceneFour = "0000000000027"
hSceneFive = "0000000000028"
# Add Project Content
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hNovelRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True
assert nwGUI.closeProject() is True
@@ -89,8 +89,8 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
# Open the Split tool
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hToSplit).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True)
monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None)
nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger)
@@ -109,7 +109,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
# No item selected
nwSplit.sourceItem = None
nwGUI.treeView.clearSelection()
nwGUI.projView.projTree.clearSelection()
assert nwSplit._populateList() is False
assert nwSplit.listBox.count() == 0
@@ -117,15 +117,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
with monkeypatch.context() as mp:
mp.setattr(NWTree, "__getitem__", lambda *a: None)
nwSplit.sourceItem = None
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hToSplit).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True)
assert nwSplit._populateList() is False
assert nwSplit.listBox.count() == 0
# Select a non-file
nwSplit.sourceItem = None
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True)
assert nwSplit._populateList() is False
assert nwSplit.listBox.count() == 0
@@ -173,52 +173,52 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
assert readFile(os.path.join(contentDir, hPartition+".nwd")) == (
"%%%%~name: Nantucket\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hPartition, tPartition)
) % (hNewFolder, hPartition, tPartition)
assert readFile(os.path.join(contentDir, hChapterOne+".nwd")) == (
"%%%%~name: Chapter One\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hChapterOne, tChapterOne)
) % (hNewFolder, hChapterOne, tChapterOne)
assert readFile(os.path.join(contentDir, hSceneOne+".nwd")) == (
"%%%%~name: Scene One\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hSceneOne, tSceneOne)
) % (hNewFolder, hSceneOne, tSceneOne)
assert readFile(os.path.join(contentDir, hSceneTwo+".nwd")) == (
"%%%%~name: Scene Two\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hSceneTwo, tSceneTwo)
) % (hNewFolder, hSceneTwo, tSceneTwo)
assert readFile(os.path.join(contentDir, hSceneThree+".nwd")) == (
"%%%%~name: Scene Three\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hSceneThree, tSceneThree)
) % (hNewFolder, hSceneThree, tSceneThree)
assert readFile(os.path.join(contentDir, hSceneFour+".nwd")) == (
"%%%%~name: Scene Four\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hSceneFour, tSceneFour)
) % (hNewFolder, hSceneFour, tSceneFour)
assert readFile(os.path.join(contentDir, hSceneFive+".nwd")) == (
"%%%%~name: The End\n"
"%%%%~path: 031b4af5197ec/%s\n"
"%%%%~path: %s/%s\n"
"%%%%~kind: NOVEL/DOCUMENT\n"
"%s\n\n"
) % (hSceneFive, tSceneFive)
) % (hNewFolder, hSceneFive, tSceneFive)
# OS error
with monkeypatch.context() as mp:
@@ -230,12 +230,6 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwSplit._doSplit() is False
# Block folder creation by returning that the folder has a depth
# of 50 items in the tree
with monkeypatch.context() as mp:
mp.setattr(NWTree, "getItemPath", lambda *a: [""]*50)
assert nwSplit._doSplit() is False
# Clear the list
nwSplit.listBox.clear()
assert nwSplit._doSplit() is False
-232
View File
@@ -1,232 +0,0 @@
"""
novelWriter Item Editor Dialog Class Tester
=============================================
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from tools import getGuiItem
from PyQt5.QtWidgets import QAction, QDialog, QMessageBox
from novelwriter.gui import GuiProjectTree
from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.dialogs import GuiItemEditor
from novelwriter.core.tree import NWTree
@pytest.mark.gui
def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj):
"""Test launching the item editor dialog from GuiMain.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# Block Dialog exec_
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
# Open Editor wo/Project
assert nwGUI.editItem() is False
# Create and Open Project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
# No Selection
nwGUI.treeView.clearSelection()
assert nwGUI.editItem() is False
# Force opening from editor
assert nwGUI.openDocument("0e17daca5f3e1")
nwGUI.isFocusMode = True
# Block Tree Lookup
with monkeypatch.context() as mp:
mp.setattr(NWTree, "__getitem__", lambda *a: None)
assert nwGUI.editItem() is False
# Invalid Type
nwGUI.theProject.projTree["0e17daca5f3e1"]._type = nwItemType.NO_TYPE
assert nwGUI.editItem() is False
nwGUI.theProject.projTree["0e17daca5f3e1"]._type = nwItemType.FILE
# Open Properly
assert nwGUI.editItem() is True
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert itemEdit is not None
itemEdit.close()
# Open Via Menu
with monkeypatch.context() as mp:
mp.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted)
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert itemEdit is not None
itemEdit.close()
nwGUI.isFocusMode = False
# END Test testDlgItemEditor_Dialog
@pytest.mark.gui
def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the item editor dialog for a novel document.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
assert nwGUI.openDocument("0e17daca5f3e1")
# Check that an invalid handle is managed
itemEdit = GuiItemEditor(nwGUI, "whatever")
itemEdit.show()
itemEdit._doClose()
# Edit a Document
itemEdit = GuiItemEditor(nwGUI, "0e17daca5f3e1")
itemEdit.show()
# Check Existing Settings
assert itemEdit.editName.text() == "New Scene"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
assert itemEdit.editExport.isChecked() is True
# Change Settings
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE)
itemEdit.editName.setText("Great Scene")
itemEdit.editStatus.setCurrentIndex(1)
itemEdit.editLayout.setCurrentIndex(layoutIdx)
itemEdit.editExport.setChecked(False)
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "Great Scene"
assert itemEdit.theItem.itemStatus == "Note"
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
assert itemEdit.theItem.isExported is False
# Check that the editor header is updated
nwGUI.docEditor.updateDocInfo("0e17daca5f3e1")
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel New Chapter Great Scene"
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Dialog
@pytest.mark.gui
def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the item editor dialog for a project note.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
# Create Note
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
# Open Note
assert nwGUI.openDocument("1a6562590ef19")
# Edit a Document
itemEdit = GuiItemEditor(nwGUI, "1a6562590ef19")
itemEdit.show()
# Check Existing Settings
assert itemEdit.editName.text() == "New File"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
assert itemEdit.editExport.isChecked() is True
# Change Settings
itemEdit.editName.setText("New Character")
itemEdit.editStatus.setCurrentIndex(1)
itemEdit.editExport.setChecked(False)
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "New Character"
assert itemEdit.theItem.itemStatus == "Minor"
assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
assert itemEdit.theItem.isExported is False
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Note
@pytest.mark.gui
def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
"""Test the item editor dialog for a folder.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
# Edit a Folder
itemEdit = GuiItemEditor(nwGUI, "31489056e0916")
itemEdit.show()
# Check Existing Settings
assert itemEdit.editName.text() == "New Chapter"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT
assert itemEdit.editExport.isChecked() is False
assert itemEdit.editLayout.isEnabled() is False
assert itemEdit.editExport.isEnabled() is False
# Change Settings
itemEdit.editName.setText("Chapter One")
itemEdit.editStatus.setCurrentIndex(1)
# Check New Settings
itemEdit._doSave()
assert itemEdit.theItem.itemName == "Chapter One"
assert itemEdit.theItem.itemStatus == "Note"
assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT
assert itemEdit.theItem.isExported is False
itemEdit.close()
del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Folder
+2 -2
View File
@@ -239,8 +239,8 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir):
copyfile(projFile, testFile)
ignTuple = (
"timestamp", "guifont", "lastnotes", "guilang", "geometry",
"preferences", "treecols", "novelcols", "projcols", "mainpane",
"docpane", "viewpane", "outlinepane", "textfont", "textsize"
"preferences", "projcols", "mainpane", "docpane", "viewpane",
"outlinepane", "textfont", "textsize"
)
assert cmpFiles(testFile, compFile, ignoreStart=ignTuple)
+2 -1
View File
@@ -42,7 +42,8 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
"""Test the load project wizard.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
assert nwGUI.openProject(nwMinimal)
assert nwGUI.closeProject()
+59 -25
View File
@@ -19,27 +19,29 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import pytest
from shutil import copyfile
from tools import cmpFiles, getGuiItem
from tools import cmpFiles, getGuiItem, buildTestProject
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QAction, QMessageBox, QColorDialog, QTreeWidgetItem
)
from PyQt5.QtWidgets import QDialog, QAction, QMessageBox, QColorDialog
from novelwriter.dialogs import GuiProjectSettings
keyDelay = 2
typeDelay = 1
stepDelay = 20
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
importKeys = ["i000004", "i000005", "i000006", "i000007"]
@pytest.mark.gui
def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir):
def testDlgProjSettings_Dialog(
qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, mockRnd
):
"""Test the full project settings dialog.
"""
projFile = os.path.join(fncProj, "nwProject.nwx")
@@ -55,8 +57,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
assert getGuiItem("GuiProjectSettings") is None
# Create new project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
buildTestProject(nwGUI, fncProj)
nwGUI.mainConf.backupPath = fncDir
nwGUI.theProject.setSpellLang("en")
@@ -80,7 +81,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
# ============
assert projEdit.tabMain.editName.text() == "New Project"
assert projEdit.tabMain.editTitle.text() == ""
assert projEdit.tabMain.editTitle.text() == "New Novel"
assert projEdit.tabMain.editAuthors.toPlainText() == "Jane Smith\nJohn Smith"
assert projEdit.tabMain.spellLang.currentData() == "en"
assert projEdit.tabMain.doBackup.isChecked() is False
@@ -89,6 +90,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
projEdit.tabMain.editName.setText("")
for c in "Project Name":
qtbot.keyClick(projEdit.tabMain.editName, c, delay=typeDelay)
projEdit.tabMain.editTitle.setText("")
for c in "Project Title":
qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=typeDelay)
@@ -111,19 +113,9 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
projEdit._tabBox.setCurrentWidget(projEdit.tabStatus)
assert projEdit.tabStatus.colChanged is False
assert projEdit.tabStatus.getNewList() is None
assert projEdit.tabStatus.getNewList() == ([], [])
assert projEdit.tabStatus.listBox.topLevelItemCount() == 4
# Fake drag'n'drop should change changed status
projEdit.tabStatus._rowsMoved()
assert projEdit.tabStatus.colChanged is True
projEdit.tabStatus.colChanged = False
projEdit.tabStatus.listBox.clearSelection()
assert projEdit.tabStatus._getSelectedItem() is None
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
assert isinstance(projEdit.tabStatus._getSelectedItem(), QTreeWidgetItem)
# Can't delete the first item (it's in use)
projEdit.tabStatus.listBox.clearSelection()
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
@@ -150,11 +142,53 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
qtbot.wait(stepDelay)
assert projEdit.tabStatus.colChanged is True
assert projEdit.tabStatus.getNewList() == [
("New", 100, 100, 100, "New"),
("Note", 200, 50, 0, "Note"),
("Finished", 50, 200, 0, "Finished"),
("Final", 20, 30, 40, None)
assert projEdit.tabStatus.getNewList() == (
[
{
"key": statusKeys[0],
"name": "New",
"cols": (100, 100, 100)
}, {
"key": statusKeys[1],
"name": "Note",
"cols": (200, 50, 0)
}, {
"key": statusKeys[3],
"name": "Finished",
"cols": (50, 200, 0)
}, {
"key": None,
"name": "Final",
"cols": (20, 30, 40)
}
], [
statusKeys[2] # Deleted item
]
)
# Move items
projEdit.tabStatus.listBox.clearSelection()
projEdit.tabStatus._moveItem(1)
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
statusKeys[0], statusKeys[1], statusKeys[3], None
]
projEdit.tabStatus.listBox.clearSelection()
projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True)
projEdit.tabStatus._moveItem(-1)
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
statusKeys[0], statusKeys[1], statusKeys[3], None
]
projEdit.tabStatus.listBox.clearSelection()
projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True)
projEdit.tabStatus._moveItem(-1)
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
statusKeys[0], statusKeys[1], None, statusKeys[3]
]
projEdit.tabStatus._moveItem(1)
assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [
statusKeys[0], statusKeys[1], statusKeys[3], None
]
# Importance Tab
+1
View File
@@ -40,6 +40,7 @@ stepDelay = 20
def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal):
"""test the word list editor.
"""
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None)
+13 -13
View File
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QMessageBox, qApp
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.core import countWords
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout
from novelwriter.constants import nwKeyWords, nwUnicode
keyDelay = 2
@@ -43,6 +43,7 @@ def testGuiEditor_Init(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
# Open project
assert nwGUI.openProject(nwMinimal)
@@ -184,10 +185,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe
assert "Could not save document." in caplog.text
# Change header level
assert nwGUI.theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT
assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT
nwGUI.docEditor.replaceText(longText[1:])
assert nwGUI.docEditor.saveText() is True
assert nwGUI.theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT
assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT
# Regular save
assert nwGUI.docEditor.saveText() is True
@@ -235,9 +236,9 @@ def testGuiEditor_MetaData(qtbot, monkeypatch, nwGUI, nwMinimal):
assert nwGUI.docEditor.setCursorPosition(None) is False
assert nwGUI.docEditor.setCursorPosition(10) is True
assert nwGUI.docEditor.getCursorPosition() == 10
assert nwGUI.theProject.projTree[sHandle].cursorPos != 10
assert nwGUI.theProject.tree[sHandle].cursorPos != 10
nwGUI.docEditor.saveCursorPosition()
assert nwGUI.theProject.projTree[sHandle].cursorPos == 10
assert nwGUI.theProject.tree[sHandle].cursorPos == 10
assert nwGUI.docEditor.setCursorLine(None) is False
assert nwGUI.docEditor.setCursorLine(2) is True
@@ -1142,11 +1143,11 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
# Create Character
theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n"
cHandle = nwGUI.theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3")
assert nwGUI.openDocument(cHandle) is True
assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.saveDocument() is True
assert nwGUI.treeView.revealNewTreeItem(cHandle)
assert nwGUI.projView.revealNewTreeItem(cHandle)
nwGUI.docEditor.updateTagHighLighting()
# Follow Tag
@@ -1225,8 +1226,8 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
# Open a document and populate it
sHandle = "8c659a11cd429"
nwGUI.theProject.projTree[sHandle]._initCount = 0 # Clear item's count
nwGUI.theProject.projTree[sHandle]._wordCount = 0 # Clear item's count
nwGUI.theProject.tree[sHandle]._initCount = 0 # Clear item's count
nwGUI.theProject.tree[sHandle]._wordCount = 0 # Clear item's count
assert nwGUI.openDocument(sHandle) is True
qtbot.wait(stepDelay)
@@ -1252,9 +1253,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
nwGUI.docEditor.wCounterDoc.run()
# nwGUI.docEditor._updateDocCounts(cC, wC, pC)
qtbot.wait(stepDelay)
assert nwGUI.theProject.projTree[sHandle]._charCount == cC
assert nwGUI.theProject.projTree[sHandle]._wordCount == wC
assert nwGUI.theProject.projTree[sHandle]._paraCount == pC
assert nwGUI.theProject.tree[sHandle]._charCount == cC
assert nwGUI.theProject.tree[sHandle]._wordCount == wC
assert nwGUI.theProject.tree[sHandle]._paraCount == pC
assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})"
# Select all text
@@ -1285,7 +1286,6 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwLipsum) is True
assert nwGUI.openDocument("4c4f28287af27") is True
origText = nwGUI.docEditor.getText()
+8 -9
View File
@@ -43,21 +43,20 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
# Open project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwLipsum)
# Rebuild the index
nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
assert nwGUI.theIndex._tagIndex != {}
assert nwGUI.theIndex._refIndex != {}
assert nwGUI.theProject.index._tagsIndex._tags != {}
assert nwGUI.theProject.index._itemIndex._items != {}
# Select a document in the project tree
nwGUI.treeView.setSelectedHandle("88243afbe5ed8")
nwGUI.projView.setSelectedHandle("88243afbe5ed8")
# Middle-click the selected item
theItem = nwGUI.treeView._getTreeItem("88243afbe5ed8")
theRect = nwGUI.treeView.visualItemRect(theItem)
qtbot.mouseClick(nwGUI.treeView.viewport(), Qt.MidButton, pos=theRect.center())
theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
theRect = nwGUI.projView.projTree.visualItemRect(theItem)
qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center())
assert nwGUI.docViewer.docHandle() == "88243afbe5ed8"
# Reload the text
@@ -118,7 +117,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False
# Open again via menu
assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8")
assert nwGUI.projView.setSelectedHandle("88243afbe5ed8")
nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger)
# Select "Bod" link
@@ -141,7 +140,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.docViewer.reloadText()
# Change document title
nwItem = nwGUI.theProject.projTree["4c4f28287af27"]
nwItem = nwGUI.theProject.tree["4c4f28287af27"]
nwItem.setName("Test Title")
assert nwItem.itemName == "Test Title"
nwGUI.docViewer.updateDocInfo("4c4f28287af27")
+172 -90
View File
@@ -19,20 +19,20 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import pytest
from shutil import copyfile
from tools import cmpFiles
from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QDialog
from PyQt5.QtWidgets import QMessageBox, QInputDialog
from novelwriter.gui import (
GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline
)
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.dialogs.itemeditor import GuiItemEditor
from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView
from novelwriter.enum import nwItemType, nwView, nwWidget
from novelwriter.tools import GuiProjectWizard
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.dialogs import GuiEditLabel
keyDelay = 2
typeDelay = 1
@@ -57,10 +57,9 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
assert nwGUI.mergeDocuments() is False
assert nwGUI.splitDocument() is False
assert nwGUI.openSelectedItem() is False
assert nwGUI.editItem() is False
assert nwGUI.editItemLabel() is False
assert nwGUI.requestNovelTreeRefresh() is False
assert nwGUI.rebuildIndex() is False
assert nwGUI.rebuildOutline() is False
assert nwGUI.showProjectSettingsDialog() is False
assert nwGUI.showProjectDetailsDialog() is False
assert nwGUI.showBuildProjectDialog() is False
@@ -71,83 +70,117 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
@pytest.mark.gui
def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj):
def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj):
"""Test creating a new project.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
# No data
with monkeypatch.context() as mp:
mp.setattr(GuiProjectWizard, "exec_", lambda *a: None)
assert nwGUI.newProject(projData=None) is False
# Close project
with monkeypatch.context() as mp:
nwGUI.hasProject = True
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwGUI.newProject(projData={"projPath": fncProj}) is False
# No project path
assert nwGUI.newProject(projData={}) is False
# Project file already exists
projFile = os.path.join(fncProj, nwGUI.theProject.projFile)
writeFile(projFile, "Stuff")
assert nwGUI.newProject(projData={"projPath": fncProj}) is False
os.unlink(projFile)
# An unreachable path should also fail
projPath = os.path.join(fncProj, "stuff", "stuff", "stuff")
assert nwGUI.newProject(projData={"projPath": projPath}) is False
# This one should work just fine
assert nwGUI.newProject(projData={"projPath": fncProj}) is True
assert os.path.isfile(os.path.join(fncProj, nwGUI.theProject.projFile))
assert os.path.isdir(os.path.join(fncProj, "content"))
# END Test testGuiMain_NewProject
@pytest.mark.gui
def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test handling of project tree items based on GUI focus states.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True
assert nwGUI.saveProject() is True
buildTestProject(nwGUI, fncProj)
sHandle = "0e17daca5f3e1"
sHandle = "000000000000f"
assert nwGUI.openSelectedItem() is False
# Project Tree has focus
nwGUI._changeView(nwView.PROJECT)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projTabs.setCurrentIndex(0)
nwGUI.projStack.setCurrentIndex(0)
with monkeypatch.context() as mp:
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
nwGUI.treeView._getTreeItem(sHandle).setSelected(True)
nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
# Novel Tree has focus
nwGUI.projTabs.setCurrentIndex(1)
nwGUI.novelView.refreshTree(True)
nwGUI._changeView(nwView.NOVEL)
nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True)
with monkeypatch.context() as mp:
mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True)
mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
actItem = nwGUI.novelView.topLevelItem(0)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
nwGUI.novelView.setCurrentItem(selItem)
selItem = nwGUI.novelView.novelTree.topLevelItem(2)
nwGUI.novelView.novelTree.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
# Project Outline has focus
nwGUI._changeView(nwView.OUTLINE)
nwGUI.switchFocus(nwWidget.OUTLINE)
with monkeypatch.context() as mp:
mp.setattr(GuiOutline, "hasFocus", lambda *a: True)
mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
actItem = nwGUI.projView.topLevelItem(0)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
nwGUI.projView.setCurrentItem(selItem)
selItem = nwGUI.outlineView.outlineTree.topLevelItem(2)
nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
# qtbot.stopForInteraction()
# qtbot.stop()
# END Test testGuiMain_ProjectTreeItems
@pytest.mark.gui
def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mockRnd):
"""Test the document editor.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
monkeypatch.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True))
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create new, save, close project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
buildTestProject(nwGUI, fncProj)
assert nwGUI.saveProject()
assert nwGUI.closeProject()
assert len(nwGUI.theProject.projTree) == 0
assert len(nwGUI.theProject.projTree._treeOrder) == 0
assert len(nwGUI.theProject.projTree._treeRoots) == 0
assert nwGUI.theProject.projTree.trashRoot() is None
assert len(nwGUI.theProject.tree) == 0
assert len(nwGUI.theProject.tree._treeOrder) == 0
assert len(nwGUI.theProject.tree._treeRoots) == 0
assert nwGUI.theProject.tree.trashRoot() is None
assert nwGUI.theProject.projPath is None
assert nwGUI.theProject.projMeta is None
assert nwGUI.theProject.projFile == "nwProject.nwx"
@@ -161,7 +194,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
testFile = os.path.join(outDir, "guiEditor_Main_Initial_nwProject.nwx")
compFile = os.path.join(refDir, "guiEditor_Main_Initial_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
qtbot.wait(stepDelay)
# qtbot.stopForInteraction()
@@ -171,27 +204,27 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
qtbot.wait(stepDelay)
# Check that we loaded the data
assert len(nwGUI.theProject.projTree) == 8
assert len(nwGUI.theProject.projTree._treeOrder) == 8
assert len(nwGUI.theProject.projTree._treeRoots) == 4
assert nwGUI.theProject.projTree.trashRoot() is None
assert len(nwGUI.theProject.tree) == 8
assert len(nwGUI.theProject.tree._treeOrder) == 8
assert len(nwGUI.theProject.tree._treeRoots) == 4
assert nwGUI.theProject.tree.trashRoot() is None
assert nwGUI.theProject.projPath == fncProj
assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta")
assert nwGUI.theProject.projFile == "nwProject.nwx"
assert nwGUI.theProject.projName == "New Project"
assert nwGUI.theProject.bookTitle == ""
assert len(nwGUI.theProject.bookAuthors) == 0
assert not nwGUI.theProject.spellCheck
assert nwGUI.theProject.bookTitle == "New Novel"
assert len(nwGUI.theProject.bookAuthors) == 1
assert nwGUI.theProject.spellCheck is False
# Check that tree items have been created
assert nwGUI.treeView._getTreeItem("73475cb40a568") is not None
assert nwGUI.treeView._getTreeItem("25fc0e7096fc6") is not None
assert nwGUI.treeView._getTreeItem("31489056e0916") is not None
assert nwGUI.treeView._getTreeItem("98010bd9270f9") is not None
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
assert nwGUI.treeView._getTreeItem("44cb730c42048") is not None
assert nwGUI.treeView._getTreeItem("71ee45a3c0db9") is not None
assert nwGUI.treeView._getTreeItem("811786ad1ae74") is not None
assert nwGUI.projView.projTree._getTreeItem("0000000000008") is not None
assert nwGUI.projView.projTree._getTreeItem("0000000000009") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000a") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000b") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000c") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000d") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000e") is not None
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
nwGUI.mainMenu.aSpellCheck.setChecked(True)
assert nwGUI.mainMenu._toggleSpellCheck()
@@ -204,9 +237,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
# Add a Character File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
# Type something into the document
@@ -226,9 +259,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
# Add a Plot File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("0000000000009").setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
# Type something into the document
@@ -248,9 +281,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
# Add a World File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("811786ad1ae74").setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("000000000000b").setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
assert nwGUI.openSelectedItem()
# Add Some Text
@@ -279,10 +312,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
# Select the 'New Scene' file
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True)
nwGUI.treeView._getTreeItem("31489056e0916").setExpanded(True)
nwGUI.treeView._getTreeItem("0e17daca5f3e1").setSelected(True)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("0000000000008").setExpanded(True)
nwGUI.projView.projTree._getTreeItem("000000000000d").setExpanded(True)
nwGUI.projView.projTree._getTreeItem("000000000000f").setSelected(True)
assert nwGUI.openSelectedItem()
# Type something into the document
@@ -417,21 +450,21 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
# Open and view the edited document
nwGUI.switchFocus(nwWidget.VIEWER)
assert nwGUI.openDocument("0e17daca5f3e1")
assert nwGUI.viewDocument("0e17daca5f3e1")
assert nwGUI.openDocument("000000000000f")
assert nwGUI.viewDocument("000000000000f")
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
assert nwGUI.closeDocViewer()
qtbot.wait(stepDelay)
# Check a Quick Create and Delete
assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
newHandle = nwGUI.treeView.getSelectedHandle()
assert nwGUI.theProject.projTree["2858dcd1057d3"] is not None
assert nwGUI.treeView.deleteItem()
assert nwGUI.treeView.setSelectedHandle(newHandle)
assert nwGUI.treeView.deleteItem()
assert nwGUI.theProject.projTree["2fca346db6561"] is not None # Trash
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
newHandle = nwGUI.projView.getSelectedHandle()
assert nwGUI.theProject.tree["0000000000020"] is not None
assert nwGUI.projView.deleteItem()
assert nwGUI.projView.setSelectedHandle(newHandle)
assert nwGUI.projView.deleteItem()
assert nwGUI.theProject.tree["0000000000024"] is not None # Trash
assert nwGUI.saveProject()
# Check the files
@@ -439,32 +472,81 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx")
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 13])
assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "<spellCheck"))
projFile = os.path.join(fncProj, "content", "031b4af5197ec.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_031b4af5197ec.nwd")
projFile = os.path.join(fncProj, "content", "000000000000f.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_000000000000f.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
projFile = os.path.join(fncProj, "content", "1a6562590ef19.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_1a6562590ef19.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_1a6562590ef19.nwd")
projFile = os.path.join(fncProj, "content", "0000000000020.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000020.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000020.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
projFile = os.path.join(fncProj, "content", "0e17daca5f3e1.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_0e17daca5f3e1.nwd")
projFile = os.path.join(fncProj, "content", "0000000000021.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000021.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000021.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
projFile = os.path.join(fncProj, "content", "41cfc0d1f2d12.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_41cfc0d1f2d12.nwd")
projFile = os.path.join(fncProj, "content", "0000000000022.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000022.nwd")
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000022.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
# qtbot.stopForInteraction()
# END Test testGuiMain_Editing
@pytest.mark.gui
def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test toggling focus mode in main window.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
buildTestProject(nwGUI, fncProj)
assert nwGUI.isFocusMode is False
# Focus Mode
# ==========
# No document open, so not allowing focus mode
assert nwGUI.toggleFocusMode() is False
# Open a file in editor and viewer
assert nwGUI.openDocument("000000000000f")
assert nwGUI.viewDocument("000000000000f")
# Enable focus mode
assert nwGUI.toggleFocusMode() is True
assert nwGUI.treePane.isVisible() is False
assert nwGUI.statusBar.isVisible() is False
assert nwGUI.mainMenu.isVisible() is False
assert nwGUI.viewsBar.isVisible() is False
assert nwGUI.splitView.isVisible() is False
# Disable focus mode
assert nwGUI.toggleFocusMode() is True
assert nwGUI.treePane.isVisible() is True
assert nwGUI.statusBar.isVisible() is True
assert nwGUI.mainMenu.isVisible() is True
assert nwGUI.viewsBar.isVisible() is True
assert nwGUI.splitView.isVisible() is True
# Full Screen Mode
# ================
assert nwGUI.mainConf.isFullScreen is False
nwGUI.toggleFullScreenMode()
assert nwGUI.mainConf.isFullScreen is True
nwGUI.toggleFullScreenMode()
assert nwGUI.mainConf.isFullScreen is False
# qtbot.stopForInteraction()
# END Test testGuiMain_FocusFullMode
+9 -12
View File
@@ -26,7 +26,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QTextBlock
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from tools import writeFile
from tools import writeFile, buildTestProject
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.enum import nwDocAction, nwDocInsert
@@ -48,7 +48,6 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum):
# Test Document Action with No Project
assert nwGUI.docEditor.docAction(nwDocAction.COPY) is False
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwLipsum) is True
qtbot.wait(stepDelay)
@@ -381,7 +380,6 @@ def testGuiMenu_ContextMenus(qtbot, monkeypatch, nwGUI, nwLipsum):
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwLipsum)
assert nwGUI.openDocument("4c4f28287af27")
qtbot.wait(stepDelay)
@@ -460,18 +458,17 @@ def testGuiMenu_ContextMenus(qtbot, monkeypatch, nwGUI, nwLipsum):
@pytest.mark.gui
def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
"""Test the Insert menu.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
buildTestProject(nwGUI, fncProj)
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
assert nwGUI.openDocument("0e17daca5f3e1") is True
assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None
assert nwGUI.openDocument("000000000000f") is True
nwGUI.docEditor.clear()
# Test Faulty Inserts
@@ -479,10 +476,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert nwGUI.docEditor.getText() == "hello world"
nwGUI.docEditor.clear()
assert not nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT)
assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False
assert nwGUI.docEditor.isEmpty()
assert not nwGUI.docEditor.insertText(None)
assert nwGUI.docEditor.insertText(None) is False
assert nwGUI.docEditor.isEmpty()
# qtbot.stopForInteraction()
@@ -680,7 +677,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert not nwGUI.importDocument()
# Open the document from before, and add some text to it
nwGUI.openDocument("0e17daca5f3e1")
nwGUI.openDocument("000000000000f")
nwGUI.docEditor.setText("Bar")
assert nwGUI.docEditor.getText() == "Bar"
@@ -712,7 +709,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
theBits = theMessage.split("<br>")
assert len(theBits) == 2
assert theBits[0] == "The currently open file is saved in:"
assert theBits[1] == os.path.join(fncProj, "content", "0e17daca5f3e1.nwd")
assert theBits[1] == os.path.join(fncProj, "content", "000000000000f.nwd")
# qtbot.stopForInteraction()
+126 -81
View File
@@ -19,74 +19,98 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import pytest
from tools import writeFile
from tools import buildTestProject, writeFile
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtGui import QFocusEvent
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QMessageBox, QToolTip
from novelwriter.enum import nwWidget, nwItemType
from novelwriter.dialogs import GuiEditLabel
from novelwriter.gui.noveltree import NovelTreeColumn
@pytest.mark.gui
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test navigating the novel tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
nwGUI.openProject(nwMinimal)
nwGUI.theProject.projTree.setSeed(42)
nwTree = nwGUI.novelView
buildTestProject(nwGUI, fncProj)
##
# Show/Hide Scrollbars
##
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
nwTree.mainConf.hideVScroll = True
nwTree.mainConf.hideHScroll = True
nwTree.initTree()
assert not nwTree.verticalScrollBar().isVisible()
assert not nwTree.horizontalScrollBar().isVisible()
writeFile(
os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"),
"# Jane Doe\n\n@tag: Jane\n\n"
)
writeFile(
os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), (
"### Scene One\n\n"
"@pov: Jane\n"
"@focus: Jane\n\n"
"% Synopsis: This is a scene."
)
)
nwTree.mainConf.hideVScroll = False
nwTree.mainConf.hideHScroll = False
nwTree.initTree()
assert nwTree.verticalScrollBar().isEnabled()
assert nwTree.horizontalScrollBar().isEnabled()
novelView = nwGUI.novelView
novelTree = novelView.novelTree
novelBar = novelView.novelBar
##
# Populate Tree
##
# Show/Hide Scrollbars
# ====================
nwGUI.projTabs.setCurrentIndex(nwGUI.idxNovelView)
nwGUI.mainConf.hideVScroll = True
nwGUI.mainConf.hideHScroll = True
novelView.initSettings()
assert not novelTree.verticalScrollBar().isVisible()
assert not novelTree.horizontalScrollBar().isVisible()
nwGUI.mainConf.hideVScroll = False
nwGUI.mainConf.hideHScroll = False
novelView.initSettings()
assert novelTree.verticalScrollBar().isEnabled()
assert novelTree.horizontalScrollBar().isEnabled()
# Populate Tree
# =============
nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView)
nwGUI.rebuildIndex()
nwTree._populateTree()
assert nwTree.topLevelItemCount() == 1
novelTree._populateTree(rootHandle=None)
assert novelTree.topLevelItemCount() == 3
# Rebuild should preserve selection
topItem = nwTree.topLevelItem(0)
topItem = novelTree.topLevelItem(0)
assert not topItem.isSelected()
topItem.setSelected(True)
assert nwTree.selectedItems()[0] == topItem
assert nwTree.getSelectedHandle() == ("a35baf2e93843", 0)
assert novelTree.selectedItems()[0] == topItem
assert novelView.getSelectedHandle() == ("000000000000c", 0)
nwTree.refreshTree()
assert nwTree.topLevelItem(0).isSelected()
# Refresh using the slot for the butoom
novelBar._refreshNovelTree()
assert novelTree.topLevelItem(0).isSelected()
##
# Open Items
##
# Open Items
# ==========
# Clear selection
nwTree.clearSelection()
scItem = nwTree.topLevelItem(0).child(0).child(0)
novelTree.clearSelection()
scItem = novelTree.topLevelItem(2)
scItem.setSelected(True)
assert scItem.isSelected()
# Clear selection with mouse
vPort = nwTree.viewport()
vPort = novelTree.viewport()
qtbot.mouseClick(vPort, Qt.LeftButton, pos=vPort.rect().center(), delay=10)
assert not scItem.isSelected()
@@ -94,8 +118,8 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docEditor.docHandle() is None
nwTree._treeDoubleClick(scItem, 0)
assert nwGUI.docEditor.docHandle() == "8c659a11cd429"
novelTree._treeDoubleClick(scItem, 0)
assert nwGUI.docEditor.docHandle() == "000000000000f"
# Open item with middle mouse button
scItem.setSelected(True)
@@ -104,58 +128,79 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10)
assert nwGUI.docViewer.docHandle() is None
scRect = nwTree.visualItemRect(scItem)
oldData = scItem.data(nwTree.C_TITLE, Qt.UserRole)
scItem.setData(nwTree.C_TITLE, Qt.UserRole, (None, "", ""))
scRect = novelTree.visualItemRect(scItem)
oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE)
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle() is None
scItem.setData(nwTree.C_TITLE, Qt.UserRole, oldData)
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle() == "8c659a11cd429"
assert nwGUI.docViewer.docHandle() == "000000000000f"
##
# Populate Tree
##
# Last Column
# ===========
# Add weird titles to first file to check hnadling of non-standard
# order of title levels.
writeFile(os.path.join(nwMinimal, "content", "a35baf2e93843.nwd"), (
"#### Section wo/Scene\n\n"
"### Scene wo/Chapter\n\n"
"## Chapter wo/Title\n\n"
"# Title\n\n"
"#### Section w/Title, wo/Scene\n\n"
"### Scene w/Title, wo/Chapter\n\n"
"## Chapter\n\n"
"#### Section w/Chapter, wo/Scene\n\n"
"### Scene\n\n"
"#### Section\n\n"
))
nwGUI.rebuildIndex()
nwTree._populateTree()
assert nwTree.topLevelItem(0).text(nwTree.C_TITLE) == "Section wo/Scene"
assert nwTree.topLevelItem(1).text(nwTree.C_TITLE) == "Scene wo/Chapter"
assert nwTree.topLevelItem(2).text(nwTree.C_TITLE) == "Chapter wo/Title"
assert nwTree.topLevelItem(3).text(nwTree.C_TITLE) == "Title"
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "")
tTitle = nwTree.topLevelItem(3)
assert tTitle.child(0).text(nwTree.C_TITLE) == "Section w/Title, wo/Scene"
assert tTitle.child(1).text(nwTree.C_TITLE) == "Scene w/Title, wo/Chapter"
assert tTitle.child(2).text(nwTree.C_TITLE) == "Chapter"
novelBar.setLastColType(NovelTreeColumn.POV)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.POV
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
"Jane", "Point of View: Jane"
)
tChap = tTitle.child(2)
assert tChap.child(0).text(nwTree.C_TITLE) == "Section w/Chapter, wo/Scene"
assert tChap.child(1).text(nwTree.C_TITLE) == "Scene"
novelBar.setLastColType(NovelTreeColumn.FOCUS)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.FOCUS
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
"Jane", "Focus: Jane"
)
tScene = tChap.child(1)
assert tScene.child(0).text(nwTree.C_TITLE) == "Section"
novelBar.setLastColType(NovelTreeColumn.PLOT)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.PLOT
assert novelTree._getLastColumnText("000000000000f", "T000001") == (
"", "Plot: "
)
##
# Close
##
novelTree._lastCol = None
assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "")
# qtbot.stopForInteraction()
# Item Meta
# =========
ttText = ""
def showText(pos, text):
nonlocal ttText
ttText = text
mIndex = novelTree.model().index(2, novelTree.C_MORE)
with monkeypatch.context() as mp:
mp.setattr(QToolTip, "showText", showText)
novelTree._treeItemClicked(mIndex)
assert ttText == (
"<p><b>Point of View</b>: Jane<br><b>Focus</b>: Jane</p>"
"<p><b>Synopsis</b>: This is a scene.</p>"
)
# Other Checks
# ============
scItem = novelTree.topLevelItem(2)
scItem.setSelected(True)
assert scItem.isSelected()
novelTree.focusOutEvent(QFocusEvent(QEvent.None_, Qt.MouseFocusReason))
assert not scItem.isSelected()
# Close
# =====
# qtbot.stop()
nwGUI.closeProject()
# END Test testGuiNovelTree_TreeItems
+206 -51
View File
@@ -19,20 +19,144 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import time
import pytest
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox
from tools import buildTestProject, writeFile
from novelwriter.enum import nwOutline
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QMessageBox, QAction
keyDelay = 2
typeDelay = 1
stepDelay = 20
from novelwriter.enum import nwItemClass, nwOutline, nwView
@pytest.mark.gui
def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir):
"""Test the outline view.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
nwGUI.rebuildIndex()
nwGUI._changeView(nwView.OUTLINE)
outlineView = nwGUI.outlineView
outlineTree = outlineView.outlineTree
outlineData = outlineView.outlineData
outlineMenu = outlineView.outlineBar.mColumns
# Toggle scrollbars
nwGUI.mainConf.hideVScroll = True
nwGUI.mainConf.hideHScroll = True
outlineView.initSettings()
assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
nwGUI.mainConf.hideVScroll = False
nwGUI.mainConf.hideHScroll = False
outlineView.initSettings()
assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
# Check focus
with monkeypatch.context() as mp:
mp.setattr(QWidget, "hasFocus", lambda *a: True)
assert outlineView.treeHasFocus() is True
outlineView.setTreeFocus() # Can't check. just ensures that it doesn't error
# Option State
# ============
pOptions = nwGUI.theProject.options
colNames = [h.name for h in nwOutline]
colItems = [h for h in nwOutline]
colWidth = {h: outlineTree.DEF_WIDTH[h] for h in nwOutline}
colHidden = {h: outlineTree.DEF_HIDDEN[h] for h in nwOutline}
assert outlineTree.topLevelItemCount() > 0
# Save header state not allowed
outlineTree._lastBuild = 0
outlineTree._saveHeaderState()
assert pOptions.getValue("GuiOutline", "headerOrder", []) == []
# Allow saving header state
outlineTree._lastBuild = time.time()
outlineTree._saveHeaderState()
assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames
assert outlineTree._treeOrder == colItems
assert outlineTree._colWidth == colWidth
assert outlineTree._colHidden == colHidden
# Get default values
optItems = pOptions.getValue("GuiOutline", "headerOrder", [])
optWidth = pOptions.getValue("GuiOutline", "columnWidth", {})
optHidden = pOptions.getValue("GuiOutline", "columnHidden", {})
# Add invalid column name
pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"])
outlineTree._loadHeaderState()
assert outlineTree._treeOrder == colItems
assert outlineTree._colHidden == colHidden
# Add duplicate column name
pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]])
outlineTree._loadHeaderState()
assert outlineTree._treeOrder == colItems
assert outlineTree._colHidden == colHidden
# Invalid column width data
pOptions.setValue("GuiOutline", "headerOrder", optItems)
pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None})
outlineTree._loadHeaderState()
assert outlineTree._treeOrder == colItems
assert outlineTree._colHidden == colHidden
# Invalid column width data
pOptions.setValue("GuiOutline", "headerOrder", optItems)
pOptions.setValue("GuiOutline", "columnWidth", optWidth)
pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None})
outlineTree._loadHeaderState()
assert outlineTree._treeOrder == colItems
assert outlineTree._colHidden == colHidden
# Valid settings
pOptions.setValue("GuiOutline", "headerOrder", optItems)
pOptions.setValue("GuiOutline", "columnWidth", optWidth)
pOptions.setValue("GuiOutline", "columnHidden", optHidden)
outlineTree._loadHeaderState()
assert outlineTree._treeOrder == colItems
assert outlineTree._colHidden == colHidden
# Header Menu
# ===========
# Trigger the menu entry for all hidden columns
for hItem in nwOutline:
if outlineTree.DEF_HIDDEN[hItem]:
outlineMenu.actionMap[hItem].activate(QAction.Trigger)
# Now no columns should be hidden
outlineTree._saveHeaderState()
assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values())
# qtbot.stop()
# END Test testGuiOutline_Main
@pytest.mark.gui
def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum):
"""Test the outline view.
"""
# Block message box
@@ -43,73 +167,104 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.mainConf.lastPath = nwLipsum
nwGUI.rebuildIndex()
nwGUI.mainTabs.setCurrentIndex(nwGUI.idxTabProj)
nwGUI._changeView(nwView.OUTLINE)
assert nwGUI.projView.topLevelItemCount() > 0
outlineView = nwGUI.outlineView
outlineBar = outlineView.outlineBar
outlineTree = outlineView.outlineTree
outlineData = outlineView.outlineData
# Context Menu
nwGUI.projView._headerRightClick(QPoint(1, 1))
nwGUI.projView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
nwGUI.projView.headerMenu.close()
qtbot.mouseClick(nwGUI.projView, Qt.LeftButton)
lipHandle = "b3643d0f92e32"
nwGUI.projView._loadHeaderState()
assert not nwGUI.projView._colHidden[nwOutline.CCOUNT]
# Check defaults in dropdown list
assert outlineBar.novelValue.itemData(0) == lipHandle
assert outlineBar.novelValue.itemData(1) is None # Separator
assert outlineBar.novelValue.itemData(2) == "" # All novels
# Add a second novel folder
newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL)
nwGUI.projView.revealNewTreeItem(newHandle)
# Check new values in dropdown list
assert outlineBar.novelValue.itemData(0) == lipHandle
assert outlineBar.novelValue.itemData(1) == newHandle
assert outlineBar.novelValue.itemData(2) is None # Separator
assert outlineBar.novelValue.itemData(3) == "" # All novels
# Add a bunch of files in a header order that hits all tree combos
docList = [
("Section 1", 4), ("Scene 1", 3), ("Chapter 1", 2), ("Part 1", 1),
("Section 2", 4), ("Scene 2", 3), ("Chapter 2", 2),
("Section 3", 4), ("Scene 3", 3),
("Section 4", 4),
]
for dTitle, hLevel in docList:
aHandle = nwGUI.theProject.newFile(dTitle, newHandle)
hHash = "#"*hLevel
writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n")
nwGUI.projView.revealNewTreeItem(aHandle)
nwGUI.rebuildIndex()
# Build the second novel
outlineBar.novelValue.setCurrentIndex(1)
outlineBar._refreshRequested()
# Go back to Lipsum
outlineBar.novelValue.setCurrentIndex(0)
outlineBar._refreshRequested()
# Check Details
# =============
# First Item
nwGUI.rebuildOutline()
selItem = nwGUI.projView.topLevelItem(0)
assert isinstance(selItem, QTreeWidgetItem)
outlineTree.refreshTree()
selItem = outlineTree.topLevelItem(0)
nwGUI.projView.setCurrentItem(selItem)
assert nwGUI.projMeta.titleLabel.text() == "<b>Title</b>"
assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum"
assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum"
assert nwGUI.projMeta.itemValue.text() == "Finished"
outlineTree.setCurrentItem(selItem)
assert outlineData.titleLabel.text() == "<b>Title</b>"
assert outlineData.titleValue.text() == "Lorem Ipsum"
assert outlineData.fileValue.text() == "Lorem Ipsum"
assert outlineData.itemValue.text() == "Finished"
assert nwGUI.projMeta.cCValue.text() == "230"
assert nwGUI.projMeta.wCValue.text() == "40"
assert nwGUI.projMeta.pCValue.text() == "3"
assert outlineData.cCValue.text() == "230"
assert outlineData.wCValue.text() == "40"
assert outlineData.pCValue.text() == "3"
# Scene One
actItem = nwGUI.projView.topLevelItem(1)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
selItem = outlineTree.topLevelItem(4)
nwGUI.projView.setCurrentItem(selItem)
tHandle, tLine = nwGUI.projView.getSelectedHandle()
outlineTree.setCurrentItem(selItem)
tHandle, tLine = outlineTree.getSelectedHandle()
assert tHandle == "88243afbe5ed8"
assert tLine == 0
assert nwGUI.projMeta.titleLabel.text() == "<b>Scene</b>"
assert nwGUI.projMeta.titleValue.text() == "Scene One"
assert nwGUI.projMeta.fileValue.text() == "Scene One"
assert nwGUI.projMeta.itemValue.text() == "Finished"
assert outlineData.titleLabel.text() == "<b>Scene</b>"
assert outlineData.titleValue.text() == "Scene One"
assert outlineData.fileValue.text() == "Scene One"
assert outlineData.itemValue.text() == "Finished"
# Click POV Link
assert nwGUI.projMeta.povKeyValue.text() == "<a href='#pov=Bod'>Bod</a>"
nwGUI.projMeta._tagClicked("#pov=Bod")
assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>"
outlineView._tagClicked("Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
# Scene One, Section Two
actItem = nwGUI.projView.topLevelItem(1)
chpItem = actItem.child(0)
scnItem = chpItem.child(0)
selItem = scnItem.child(0)
selItem = outlineTree.topLevelItem(5)
nwGUI.projView.setCurrentItem(selItem)
tHandle, tLine = nwGUI.projView.getSelectedHandle()
outlineTree.setCurrentItem(selItem)
tHandle, tLine = outlineTree.getSelectedHandle()
assert tHandle == "88243afbe5ed8"
assert tLine == 12
assert nwGUI.projMeta.titleLabel.text() == "<b>Section</b>"
assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two"
assert nwGUI.projMeta.fileValue.text() == "Scene One"
assert nwGUI.projMeta.itemValue.text() == "Finished"
assert outlineData.titleLabel.text() == "<b>Section</b>"
assert outlineData.titleValue.text() == "Scene One, Section Two"
assert outlineData.fileValue.text() == "Scene One"
assert outlineData.itemValue.text() == "Finished"
nwGUI.projView._treeDoubleClick(selItem, 0)
outlineTree._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
# qtbot.stopForInteraction()
# qtbot.stop()
# END Test testGuiOutline_Main
# END Test testGuiOutline_Content
+493 -183
View File
@@ -22,218 +22,528 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest
import os
from tools import writeFile
from tools import buildTestProject
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox
from PyQt5.QtWidgets import QMessageBox, QMenu
from novelwriter.guimain import GuiMain
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
from novelwriter.dialogs import GuiEditLabel
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.enum import nwItemType, nwItemClass
@pytest.mark.gui
def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
nwGUI.theProject.projTree.setSeed(42)
nwTree = nwGUI.treeView
nwTree = nwGUI.projView
##
# Add New Items
##
# Try to add item with no project
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
# Try to add and move item with no project
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.moveTreeItem(1)
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# Open a project
assert nwGUI.openProject(nwMinimal)
# No itemType set
nwTree.projTree.clearSelection()
assert nwTree.projTree.newTreeItem(None) is False
# Root Items
# ==========
# No class set
assert nwTree.projTree.newTreeItem(nwItemType.ROOT) is False
# Create root item
assert nwTree.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
assert "0000000000010" in nwGUI.theProject.tree
# File/Folder Items
# =================
# No location selected for new item
nwTree.clearSelection()
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
nwTree.projTree.clearSelection()
caplog.clear()
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is False
assert "Did not find anywhere" in caplog.text
# No itemType set or ROOT, but no class
assert not nwTree.newTreeItem(None, None)
assert not nwTree.newTreeItem(nwItemType.ROOT, None)
# Create new folder as child of Novel folder
nwTree.setSelectedHandle("0000000000008")
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
assert nwGUI.theProject.tree["0000000000011"].itemParent == "0000000000008"
assert nwGUI.theProject.tree["0000000000011"].itemRoot == "0000000000008"
assert nwGUI.theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL
# Select a location
chItem = nwTree._getTreeItem("a6d311a93600a")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
chItem.setExpanded(True)
# Add a new file in the new folder
nwTree.setSelectedHandle("0000000000011")
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwGUI.theProject.tree["0000000000012"].itemParent == "0000000000011"
assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008"
assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL
# Create new item with no class set (defaults to NOVEL)
assert nwTree.newTreeItem(nwItemType.FILE, None)
assert nwTree.newTreeItem(nwItemType.FOLDER, None)
# Add a new chapter next to the other new file
nwTree.setSelectedHandle("0000000000012")
assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011"
assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008"
assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000013")
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
# Check that we have the correct tree order
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
# Add a new scene next to the other new file
nwTree.setSelectedHandle("0000000000012")
assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011"
assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008"
assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000014")
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
# Add roots
assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid
# Add a new file to the characters folder
nwTree.setSelectedHandle("000000000000a")
assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a"
assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a"
assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER
assert nwGUI.openDocument("0000000000015")
assert nwGUI.docEditor.getText() == "# New Note\n\n"
# Change max depth and try to add a subfolder that is too deep
monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2)
chItem = nwTree._getTreeItem("71ee45a3c0db9")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
# Make sure the sibling folder bug trap works
nwTree.setSelectedHandle("0000000000013")
nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen
caplog.clear()
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
assert "Internal error" in caplog.text
nwGUI.theProject.tree["0000000000013"].setParent("0000000000011")
##
# Move Items
##
# Cancel during creation
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
nwTree.setSelectedHandle("0000000000013")
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
nwTree.setSelectedHandle("8c659a11cd429")
# Get the trash folder
nwTree.projTree._addTrashRoot()
trashHandle = nwGUI.theProject.trashFolder()
nwTree.setSelectedHandle(trashHandle)
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False
assert "Cannot add new files or folders to the Trash folder" in caplog.text
# Shift focus and try to move item
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
assert not nwTree.moveTreeItem(1)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# Other Checks
# ============
# Move second item up twice (should give same result)
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
]
# Also check error handling in reveal function
assert nwTree.revealNewTreeItem("abc") is False
# Move it back down four times (last two should be the same)
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move up twice, and undo
nwTree._lastMove = {}
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move a root item (top level items are different) twice
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
nwTree.setSelectedHandle("9d5247ab588e0")
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
##
# Delete and Trash
##
# Add some content to the new file
nwGUI.openDocument("73475cb40a568")
nwGUI.docEditor.setText("# Hello World\n")
nwGUI.saveDocument()
nwGUI.saveProject()
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
# Delete the items we added earlier
nwTree.clearSelection()
assert not nwTree.emptyTrash() # No folder yet
assert not nwTree.deleteItem(None)
assert not nwTree.deleteItem("1111111111111")
assert nwTree.deleteItem("73475cb40a568") # New File
assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
assert nwTree.deleteItem("811786ad1ae74") # Custom Root
assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder
assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder
# The file is in trash, empty it
assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
assert nwTree.emptyTrash()
assert not nwTree.emptyTrash() # Already empty
assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder
# Should not be allowed to add files and folders to Trash
trashHandle = nwGUI.theProject.projTree.trashRoot()
chItem = nwTree._getTreeItem(trashHandle)
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
assert not nwTree.newTreeItem(nwItemType.FILE, None)
assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
# Close the project
nwGUI.closeProject()
##
# Orphaned Files
##
# Add an orphaned file
orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
writeFile(orphFile, "# Hello World\n")
# Open the project again
nwGUI.openProject(nwMinimal)
# Check that the orphaned file was found and added to the tree
nwTree.flushTreeOrder()
assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
orItem = nwTree._getTreeItem("1234567890abc")
assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
##
# Unexpected Error Handling
##
# Add an item with an invalid type
assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL)
assert "Failed to add new item" in caplog.messages[-1]
# Add new file after one that has no parent handle
chItem = nwTree._getTreeItem("44cb730c42048")
nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
nwTree.theProject.projTree["44cb730c42048"]._parent = None
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
nwTree.clearSelection()
# Add a file with no parent, and fail to find a suitable parent item
monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None)
assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL)
# Add an item that cannot be displayed in the tree
nHandle = nwGUI.theProject.newFile("Test", None)
assert nwTree.revealNewTreeItem(nHandle) is False
# Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_TreeItems
# END Test testGuiProjTree_NewItems
@pytest.mark.gui
def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
nwTree = nwGUI.projView
# Try to move item with no project
assert nwTree.projTree.moveTreeItem(1) is False
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# Move Documents
# ==============
# Add some files
nwTree.setSelectedHandle("000000000000d")
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000011", "0000000000012",
]
# Move with no selections
nwTree.projTree.clearSelection()
assert nwTree.projTree.moveTreeItem(1) is False
# Move second item up twice (should give same result)
nwTree.setSelectedHandle("000000000000f")
assert nwTree.projTree.moveTreeItem(-1) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000f", "000000000000e",
"0000000000010", "0000000000011", "0000000000012",
]
assert nwTree.projTree.moveTreeItem(-1) is False
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000f", "000000000000e",
"0000000000010", "0000000000011", "0000000000012",
]
# Restore
assert nwTree.projTree.moveTreeItem(1) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000011", "0000000000012",
]
# Move fifth item down twice (should give same result)
nwTree.setSelectedHandle("0000000000011")
assert nwTree.projTree.moveTreeItem(1) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000012", "0000000000011",
]
assert nwTree.projTree.moveTreeItem(1) is False
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000012", "0000000000011",
]
# Restore
assert nwTree.projTree.moveTreeItem(-1) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000011", "0000000000012",
]
# Move down again, and restore via undo
nwTree.setSelectedHandle("0000000000011")
assert nwTree.projTree.moveTreeItem(1) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000012", "0000000000011",
]
assert nwTree.projTree.undoLastMove() is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000011", "0000000000012",
]
# Root Folder
# ===========
nwTree.setSelectedHandle("0000000000008")
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Move novel folder up
assert nwTree.projTree.moveTreeItem(-1) is False
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Move novel folder down
assert nwTree.projTree.moveTreeItem(1) is True
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1
# Move novel folder up again
assert nwTree.projTree.moveTreeItem(-1) is True
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_MoveItems
@pytest.mark.gui
def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
"""Test adding and removing items from the project tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
nwTree = nwGUI.projView
# Try to run with no project
assert nwTree.emptyTrash() is False
assert nwTree.deleteItem() is False
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# Try emptying the trash already now, when there is no trash folder
assert nwTree.emptyTrash() is False
# Add some files
nwTree.setSelectedHandle("000000000000d")
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010", "0000000000011", "0000000000012",
]
# Delete item without focus -> blocked
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
nwTree.setSelectedHandle("0000000000012")
assert nwTree.deleteItem() is False
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
# No selection made
nwTree.projTree.clearSelection()
caplog.clear()
assert nwTree.deleteItem() is False
assert "no item to delete" in caplog.text
# Not a valid handle
nwTree.projTree.clearSelection()
caplog.clear()
assert nwTree.deleteItem("0000000000000") is False
assert "Could not find tree item" in caplog.text
# Delete Folder/Root
# ==================
# Deleting non-empty folders is blocked
assert nwTree.deleteItem("0000000000008") is False # Novel Root
assert nwTree.deleteItem("000000000000a") is True # Character Root
# Delete File
# ===========
# Block adding trash folder
funcPointer = nwTree.projTree._addTrashRoot
nwTree.projTree._addTrashRoot = lambda *a: None
assert nwTree.deleteItem("0000000000012") is False
nwTree.projTree._addTrashRoot = funcPointer
# Delete last two documents, which also adds the trash folder
assert nwTree.deleteItem("0000000000012") is True
assert nwTree.deleteItem("0000000000011") is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e", "000000000000f",
"0000000000010"
]
trashHandle = nwGUI.theProject.tree.trashRoot()
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0000000000012", "0000000000011"
]
# Delete the first file again (permanent), and ask for permission
# Also open the document in the editor, which should trigger a close
assert os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd"))
assert "0000000000012" in nwGUI.theProject.tree
assert nwGUI.docEditor.docHandle() is None
assert nwGUI.openDocument("0000000000012") is True
assert nwGUI.docEditor.docHandle() == "0000000000012"
assert nwTree.deleteItem("0000000000012") is True
assert nwGUI.docEditor.docHandle() is None
assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000012.nwd"))
assert "0000000000012" not in nwGUI.theProject.tree
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0000000000011"
]
# Delete the second file, and skip asking for permission
assert os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd"))
assert "0000000000011" in nwGUI.theProject.tree
assert nwTree.deleteItem("0000000000011", alreadyAsked=True) is True
assert not os.path.isfile(os.path.join(prjDir, "content", "0000000000011.nwd"))
assert "0000000000011" not in nwGUI.theProject.tree
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
# Delete Folder
# =============
trashHandle = nwGUI.theProject.tree.trashRoot()
# Add a folder with two files
nwTree.setSelectedHandle("0000000000009")
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
nwTree.setSelectedHandle("0000000000014")
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd"))
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd"))
# Delete the folder, which moves everything to Trash
assert nwTree.getTreeFromHandle("0000000000014") == [
"0000000000014", "0000000000015", "0000000000016"
]
assert nwTree.deleteItem("0000000000014") is True
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0000000000014", "0000000000015", "0000000000016"
]
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd"))
assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd"))
# Delete again, which should delete folder and all files
assert nwTree.deleteItem("0000000000014") is True
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd"))
assert not os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd"))
# Add an empty folder, which can be deleted with no further restrictions
nwTree.setSelectedHandle("0000000000009")
assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True
assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009", "0000000000017"]
nwTree.setSelectedHandle("0000000000017")
assert nwTree.deleteItem("0000000000017") is True
assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009"]
# Empty Trash
# ===========
# Try to empty trash that is already empty
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
assert nwTree.emptyTrash() is False
# Move the two remaining scene documents to trash
assert nwTree.deleteItem("000000000000f") is True
assert nwTree.deleteItem("0000000000010") is True
assert nwTree.getTreeFromHandle("000000000000d") == [
"000000000000d", "000000000000e"
]
assert nwTree.getTreeFromHandle(trashHandle) == [
trashHandle, "000000000000f", "0000000000010"
]
# Empty trash, but select no on question
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwTree.emptyTrash() is False
# Empty the trash proper
assert nwTree.emptyTrash() is True
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
# Try to delete a file, but block the underlying deletion of the file on disk
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False)
assert nwTree.deleteItem("000000000000e") is True
assert nwTree.deleteItem("000000000000e") is True
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
# Delete proper
assert nwTree.projTree._deleteTreeItem("000000000000e") is True
assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
# Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiProjTree_DeleteItems
@pytest.mark.gui
def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd):
"""Test the building of the project tree context menu. All this does
is test that the menu builds. It doesn't open the actual menu,
"""
# Block message box
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
# Handles for new objects
hNovelRoot = "0000000000008"
hTitlePage = "000000000000c"
hChapterDir = "000000000000d"
hChapterFile = "000000000000e"
hCharRoot = "000000000000a"
hCharNote = "0000000000011"
hNovelNote = "0000000000012"
projTree = nwGUI.projView.projTree
projTree._getTreeItem(hNovelRoot).setExpanded(True)
projTree._getTreeItem(hChapterDir).setExpanded(True)
projTree._addTrashRoot()
hTrashRoot = projTree.theProject.tree.trashRoot()
projTree.setSelectedHandle(hCharRoot)
projTree.newTreeItem(nwItemType.FILE)
projTree.setSelectedHandle(hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
# No item under menu
assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False
# Generate the possible menu combinarions
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
assert projTree._openContextMenu(itemPos(hNovelRoot)) is True
assert projTree._openContextMenu(itemPos(hNovelNote)) is True
assert projTree._openContextMenu(itemPos(hTitlePage)) is True
assert projTree._openContextMenu(itemPos(hChapterDir)) is True
assert projTree._openContextMenu(itemPos(hChapterFile)) is True
assert projTree._openContextMenu(itemPos(hCharRoot)) is True
assert projTree._openContextMenu(itemPos(hCharNote)) is True
# Check the keyboard shortcut handler as well
projTree.setSelectedHandle(hNovelRoot)
assert projTree.openContextOnSelected() is True
projTree.clearSelection()
assert projTree.openContextOnSelected() is False
# Direct Edit Functions
# =====================
# Trigger the dedicated functions the menu entries connect to
nwItem = projTree.theProject.tree[hNovelNote]
# Toggle exported flag
assert nwItem.isExported is True
projTree._toggleItemExported(hNovelNote)
assert nwItem.isExported is False
# Change item status
assert nwItem.itemStatus == "s000000"
projTree._changeItemStatus(hNovelNote, "s000001")
assert nwItem.itemStatus == "s000001"
# Change item importance
assert nwItem.itemImport == "i000004"
projTree._changeItemImport(hNovelNote, "i000005")
assert nwItem.itemImport == "i000005"
# Change item layout
assert nwItem.itemLayout == nwItemLayout.NOTE
projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT)
assert nwItem.itemLayout == nwItemLayout.DOCUMENT
projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE)
assert nwItem.itemLayout == nwItemLayout.NOTE
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu
+9 -8
View File
@@ -22,24 +22,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import time
import pytest
from tools import buildTestProject
from PyQt5.QtWidgets import QMessageBox
from novelwriter.core import NWDoc
from novelwriter.enum import nwItemClass, nwState
from novelwriter.enum import nwState
@pytest.mark.gui
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test the the various features of the status bar.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True
cHandle = nwGUI.theProject.newFile("A Note", nwItemClass.CHARACTER, "71ee45a3c0db9")
buildTestProject(nwGUI, fncProj)
cHandle = nwGUI.theProject.newFile("A Note", "000000000000a")
newDoc = NWDoc(nwGUI.theProject, cHandle)
newDoc.writeDocument("# A Note\n\n")
nwGUI.treeView.revealNewTreeItem(cHandle)
nwGUI.projView.revealNewTreeItem(cHandle)
nwGUI.rebuildIndex(beQuiet=True)
# Reference Time
@@ -90,10 +91,10 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
# Project Stats
nwGUI.statusBar.mainConf.incNotesWCount = False
nwGUI._updateStatusWordCount()
assert nwGUI.statusBar.statsText.text() == "Words: 6 (+6)"
assert nwGUI.statusBar.statsText.text() == "Words: 9 (+9)"
nwGUI.statusBar.mainConf.incNotesWCount = True
nwGUI._updateStatusWordCount()
assert nwGUI.statusBar.statsText.text() == "Words: 8 (+8)"
assert nwGUI.statusBar.statsText.text() == "Words: 11 (+11)"
# qtbot.stopForInteraction()
+34 -33
View File
@@ -35,6 +35,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
"""Test the theme and icon classes.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
@@ -80,7 +81,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
assert thePalette.window().color() == QColor(54, 54, 54)
assert thePalette.windowText().color() == QColor(174, 174, 174)
assert thePalette.base().color() == QColor(62, 62, 62)
assert thePalette.alternateBase().color() == QColor(67, 67, 67)
assert thePalette.alternateBase().color() == QColor(78, 78, 78)
assert thePalette.text().color() == QColor(174, 174, 174)
assert thePalette.toolTipBase().color() == QColor(255, 255, 192)
assert thePalette.toolTipText().color() == QColor(21, 21, 13)
@@ -92,82 +93,82 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir):
assert thePalette.link().color() == QColor(44, 152, 247)
assert thePalette.linkVisited().color() == QColor(44, 152, 247)
assert nwGUI.theTheme.statNone == [150, 152, 150]
assert nwGUI.theTheme.statSaved == [39, 135, 78]
assert nwGUI.theTheme.statUnsaved == [138, 32, 32]
assert nwGUI.mainTheme.statNone == [150, 152, 150]
assert nwGUI.mainTheme.statSaved == [39, 135, 78]
assert nwGUI.mainTheme.statUnsaved == [138, 32, 32]
# Check Syntax Colours
assert nwGUI.theTheme.colBack == [45, 45, 45]
assert nwGUI.theTheme.colText == [204, 204, 204]
assert nwGUI.theTheme.colLink == [102, 153, 204]
assert nwGUI.theTheme.colHead == [102, 153, 204]
assert nwGUI.theTheme.colHeadH == [102, 153, 204]
assert nwGUI.theTheme.colEmph == [249, 145, 57]
assert nwGUI.theTheme.colDialN == [242, 119, 122]
assert nwGUI.theTheme.colDialD == [153, 204, 153]
assert nwGUI.theTheme.colDialS == [255, 204, 102]
assert nwGUI.theTheme.colHidden == [153, 153, 153]
assert nwGUI.theTheme.colKey == [242, 119, 122]
assert nwGUI.theTheme.colVal == [204, 153, 204]
assert nwGUI.theTheme.colSpell == [242, 119, 122]
assert nwGUI.theTheme.colError == [153, 204, 153]
assert nwGUI.theTheme.colRepTag == [102, 204, 204]
assert nwGUI.theTheme.colMod == [249, 145, 57]
assert nwGUI.mainTheme.colBack == [45, 45, 45]
assert nwGUI.mainTheme.colText == [204, 204, 204]
assert nwGUI.mainTheme.colLink == [102, 153, 204]
assert nwGUI.mainTheme.colHead == [102, 153, 204]
assert nwGUI.mainTheme.colHeadH == [102, 153, 204]
assert nwGUI.mainTheme.colEmph == [249, 145, 57]
assert nwGUI.mainTheme.colDialN == [242, 119, 122]
assert nwGUI.mainTheme.colDialD == [153, 204, 153]
assert nwGUI.mainTheme.colDialS == [255, 204, 102]
assert nwGUI.mainTheme.colHidden == [153, 153, 153]
assert nwGUI.mainTheme.colKey == [242, 119, 122]
assert nwGUI.mainTheme.colVal == [204, 153, 204]
assert nwGUI.mainTheme.colSpell == [242, 119, 122]
assert nwGUI.mainTheme.colError == [153, 204, 153]
assert nwGUI.mainTheme.colRepTag == [102, 204, 204]
assert nwGUI.mainTheme.colMod == [249, 145, 57]
# Test Icon class
theIcons = nwGUI.theTheme.theIcons
iconCache = nwGUI.mainTheme.iconCache
novelwriter.CONFIG.guiIcons = "invalid"
assert theIcons.updateTheme() is True
assert iconCache.updateTheme() is True
assert novelwriter.CONFIG.guiIcons == "typicons_light"
# Ask for a non-existent key
anImg = theIcons.loadDecoration("nonsense", 20, 20)
anImg = iconCache.loadDecoration("nonsense", 20, 20)
assert isinstance(anImg, QPixmap)
assert anImg.isNull()
# Add a non-existent file and request it
theIcons.DECO_MAP["nonsense"] = "nofile.jpg"
anImg = theIcons.loadDecoration("nonsense", 20, 20)
iconCache.IMAGE_MAP["nonsense"] = "nofile.jpg"
anImg = iconCache.loadDecoration("nonsense", 20, 20)
assert isinstance(anImg, QPixmap)
assert anImg.isNull()
# Get a real image, with different size parameters
anImg = theIcons.loadDecoration("wiz-back", 20, None)
anImg = iconCache.loadDecoration("wiz-back", 20, None)
assert isinstance(anImg, QPixmap)
assert not anImg.isNull()
assert anImg.width() == 20
assert anImg.height() >= 56
anImg = theIcons.loadDecoration("wiz-back", None, 70)
anImg = iconCache.loadDecoration("wiz-back", None, 70)
assert isinstance(anImg, QPixmap)
assert not anImg.isNull()
assert anImg.height() == 70
assert anImg.width() >= 24
anImg = theIcons.loadDecoration("wiz-back", 30, 70)
anImg = iconCache.loadDecoration("wiz-back", 30, 70)
assert isinstance(anImg, QPixmap)
assert not anImg.isNull()
assert anImg.height() == 70
assert anImg.width() == 30
anImg = theIcons.loadDecoration("wiz-back", None, None)
anImg = iconCache.loadDecoration("wiz-back", None, None)
assert isinstance(anImg, QPixmap)
assert not anImg.isNull()
assert anImg.height() >= 1500
assert anImg.width() >= 500
# Load icons
anIcon = theIcons.getIcon("nonsense")
anIcon = iconCache.getIcon("nonsense")
assert isinstance(anIcon, QIcon)
assert anIcon.isNull()
anIcon = theIcons.getIcon("novelwriter")
anIcon = iconCache.getIcon("novelwriter")
assert isinstance(anIcon, QIcon)
assert not anIcon.isNull()
# Check return empty icon if file not found
theIcons.ICON_KEYS.add("testicon3")
anIcon = theIcons.getIcon("testicon3")
iconCache.ICON_KEYS.add("testicon3")
anIcon = iconCache.getIcon("testicon3")
assert isinstance(anIcon, QIcon)
assert anIcon.isNull()
+75
View File
@@ -0,0 +1,75 @@
"""
novelWriter Lorem Ipsum Tool Tester
=====================================
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from tools import getGuiItem, buildTestProject
from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.tools import GuiLipsum
@pytest.mark.gui
def testToolLipsum_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
"""Test the Lorem Ipsum tool.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# Check that we cannot open when there is no project
nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger)
assert getGuiItem("GuiLipsum") is None
# Create a new project
buildTestProject(nwGUI, fncProj)
assert nwGUI.openDocument("000000000000f") is True
assert len(nwGUI.docEditor.getText()) == 15
# Open the tool
nwGUI.mainMenu.aLipsumText.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiLipsum") is not None, timeout=1000)
nwLipsum = getGuiItem("GuiLipsum")
assert isinstance(nwLipsum, GuiLipsum)
# Insert paragraphs
nwGUI.docEditor.setCursorPosition(100) # End of document
nwLipsum.paraCount.setValue(2)
nwLipsum._doInsert()
theText = nwGUI.docEditor.getText()
assert "Lorem ipsum" in theText
assert len(theText) == 965
# Insert random paragraph
nwGUI.docEditor.setCursorPosition(1000) # End of document
nwLipsum.randSwitch.setChecked(True)
nwLipsum.paraCount.setValue(1)
nwLipsum._doInsert()
theText = nwGUI.docEditor.getText()
assert len(theText) > 965
# Close
nwLipsum._doClose()
# qtbot.stopForInteraction()
# END Test testToolLipsum_Main
+178 -144
View File
@@ -19,14 +19,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import os
import sys
import pytest
from tools import getGuiItem
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox
from PyQt5.QtWidgets import QFileDialog, QWizard, QMessageBox, QDialog
from novelwriter.enum import nwItemClass
from novelwriter.tools.projwizard import (
@@ -41,8 +41,8 @@ stepDelay = 20
@pytest.mark.gui
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
"""Test the new project wizard.
def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal):
"""Test the launch of the project wizard.
Disabled for macOS because the test segfaults on QWizard.show()
"""
# Block message box
@@ -55,173 +55,207 @@ def testToolProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
# New with a project open should cause an error
assert nwGUI.openProject(nwMinimal)
assert not nwGUI.newProject()
with monkeypatch.context() as mp:
mp.setattr(nwGUI, "closeProject", lambda *a: False)
assert nwGUI.newProject() is False
# Close project, but call with invalid path
assert nwGUI.closeProject()
with monkeypatch.context() as mp:
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: None)
assert not nwGUI.newProject()
assert nwGUI.newProject() is False
# Now, with an empty dictionary
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {})
assert not nwGUI.newProject()
assert nwGUI.newProject() is False
# Now, with a non-empty folder
mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": nwMinimal})
assert not nwGUI.newProject()
assert nwGUI.newProject() is False
##
# Test the Wizard
# Test the Wizard Launching
##
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
nwGUI.mainConf.lastPath = " "
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
nwGUI.closeProject()
nwGUI.showNewProjectDialog()
result = nwGUI.showNewProjectDialog()
qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000)
nwWiz = getGuiItem("GuiProjectWizard")
assert isinstance(nwWiz, GuiProjectWizard)
nwWiz.show()
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.CancelButton), Qt.LeftButton)
assert result is None
for wStep in range(4):
# This does not actually create the project, it just generates the
# dictionary that defines it.
# Intro Page
introPage = nwWiz.currentPage()
assert isinstance(introPage, ProjWizardIntroPage)
assert not nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
for c in ("Test Minimal %d" % wStep):
qtbot.keyClick(introPage.projName, c, delay=typeDelay)
qtbot.wait(stepDelay)
for c in "Minimal Novel":
qtbot.keyClick(introPage.projTitle, c, delay=typeDelay)
qtbot.wait(stepDelay)
for c in "Jane Doe":
qtbot.keyClick(introPage.projAuthors, c, delay=typeDelay)
# Setting projName should activate the button
assert nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Folder Page
storagePage = nwWiz.currentPage()
assert isinstance(storagePage, ProjWizardFolderPage)
assert not nwWiz.button(QWizard.NextButton).isEnabled()
if wStep == 0:
# Check invalid path first, the first time we reach here
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: "")
qtbot.wait(stepDelay)
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
assert storagePage.projPath.text() == ""
# Then, we always return nwMinimal as path
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: nwMinimal)
qtbot.wait(stepDelay)
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
projPath = os.path.join(nwMinimal, "Test Minimal %d" % wStep)
assert storagePage.projPath.text() == projPath
# Setting projPath should activate the button
assert nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Populate Page
popPage = nwWiz.currentPage()
assert isinstance(popPage, ProjWizardPopulatePage)
assert nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
if wStep == 0:
popPage.popMinimal.setChecked(True)
elif wStep == 1:
popPage.popCustom.setChecked(True)
elif wStep == 2:
popPage.popCustom.setChecked(True)
elif wStep == 3:
popPage.popSample.setChecked(True)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Custom Page
if wStep == 1 or wStep == 2:
customPage = nwWiz.currentPage()
assert isinstance(customPage, ProjWizardCustomPage)
assert nwWiz.button(QWizard.NextButton).isEnabled()
customPage.addPlot.setChecked(True)
customPage.addChar.setChecked(True)
customPage.addWorld.setChecked(True)
customPage.addTime.setChecked(True)
customPage.addObject.setChecked(True)
customPage.addEntity.setChecked(True)
if wStep == 2:
customPage.numChapters.setValue(0)
customPage.numScenes.setValue(10)
customPage.chFolders.setChecked(False)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Final Page
finalPage = nwWiz.currentPage()
assert isinstance(finalPage, ProjWizardFinalPage)
assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it
# Check Data
projData = nwGUI._assembleProjectWizardData(nwWiz)
assert projData["projName"] == "Test Minimal %d" % wStep
assert projData["projTitle"] == "Minimal Novel"
assert projData["projAuthors"] == "Jane Doe"
assert projData["projPath"] == projPath
assert projData["popMinimal"] == (wStep == 0)
assert projData["popCustom"] == (wStep == 1 or wStep == 2)
assert projData["popSample"] == (wStep == 3)
if wStep == 1 or wStep == 2:
assert projData["addRoots"] == [
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
nwItemClass.TIMELINE,
nwItemClass.OBJECT,
nwItemClass.ENTITY,
]
if wStep == 1:
assert projData["numChapters"] == 5
assert projData["numScenes"] == 5
assert projData["chFolders"]
else:
assert projData["numChapters"] == 0
assert projData["numScenes"] == 10
assert not projData["chFolders"]
else:
assert projData["addRoots"] == []
assert projData["numChapters"] == 0
assert projData["numScenes"] == 0
assert not projData["chFolders"]
# Restart the wizard for next iteration
nwWiz.restart()
with monkeypatch.context() as mp:
mp.setattr(GuiProjectWizard, "result", lambda *a: QDialog.Accepted)
result = nwGUI.showNewProjectDialog()
nwWiz.button(QWizard.CancelButton).click()
assert isinstance(result, dict)
nwWiz.reject()
nwWiz.close()
# qtbot.stopForInteraction()
# END Test testToolProjectWizard_Main
# END Test testToolProjectWizard_Handling
@pytest.mark.gui
@pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"])
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
"""Test the new project wizard with a set of selection scenarios.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
nwGUI.mainConf.lastPath = " "
nwWiz = GuiProjectWizard(nwGUI)
nwWiz.show()
qtbot.wait(stepDelay)
# Intro Page
# ==========
introPage = nwWiz.currentPage()
assert isinstance(introPage, ProjWizardIntroPage)
assert not nwWiz.button(QWizard.NextButton).isEnabled()
introPage.projName.setText("Test Wizard")
introPage.projTitle.setText("My Novel")
introPage.projAuthors.setPlainText("Jane Doe")
# Setting projName should activate the button
assert nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Folder Page
# ===========
storagePage = nwWiz.currentPage()
assert isinstance(storagePage, ProjWizardFolderPage)
assert not nwWiz.button(QWizard.NextButton).isEnabled()
assert storagePage.errLabel.text() == ""
# Set an invalid path
storagePage.projPath.setText(os.path.join(fncDir, "not", "a", "path"))
assert not nwWiz.button(QWizard.NextButton).isEnabled()
assert storagePage.errLabel.text().startswith("Error")
# Set an existing path
storagePage.projPath.setText(fncDir)
assert not nwWiz.button(QWizard.NextButton).isEnabled()
assert storagePage.errLabel.text().startswith("Error")
# Return a non-result from browse
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: "")
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
assert storagePage.errLabel.text() == ""
# Let the browse feature handle it
projPath = os.path.join(fncDir, "Test Wizard")
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: fncDir)
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
assert storagePage.projPath.text() == projPath
assert storagePage.errLabel.text() == ""
# Setting projPath should activate the button
assert nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Populate Page
# =============
popPage = nwWiz.currentPage()
assert isinstance(popPage, ProjWizardPopulatePage)
assert nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
if prjType.startswith("minimal"):
popPage.popMinimal.setChecked(True)
elif prjType.startswith("custom"):
popPage.popCustom.setChecked(True)
elif prjType.startswith("sample"):
popPage.popSample.setChecked(True)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Custom Page
# ===========
if prjType.startswith("custom"):
customPage = nwWiz.currentPage()
assert isinstance(customPage, ProjWizardCustomPage)
assert nwWiz.button(QWizard.NextButton).isEnabled()
customPage.addPlot.setChecked(True)
customPage.addChar.setChecked(True)
customPage.addWorld.setChecked(True)
customPage.addNotes.setChecked(True)
if prjType == "custom2":
customPage.numChapters.setValue(0)
customPage.numScenes.setValue(10)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Final Page
# ==========
finalPage = nwWiz.currentPage()
assert isinstance(finalPage, ProjWizardFinalPage)
assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it
# Check Data
# ==========
projData = nwGUI._assembleProjectWizardData(nwWiz)
assert projData["projName"] == "Test Wizard"
assert projData["projTitle"] == "My Novel"
assert projData["projAuthors"] == "Jane Doe"
assert projData["projPath"] == projPath
assert projData["popMinimal"] == prjType.startswith("minimal")
assert projData["popCustom"] == prjType.startswith("custom")
assert projData["popSample"] == prjType.startswith("sample")
if prjType.startswith("custom"):
assert projData["addRoots"] == [
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
]
if prjType == "custom1":
assert projData["numChapters"] == 5
assert projData["numScenes"] == 5
assert projData["addNotes"] is True
else:
assert projData["numChapters"] == 0
assert projData["numScenes"] == 10
assert projData["addNotes"] is True
else:
assert projData["addRoots"] == []
assert projData["numChapters"] == 0
assert projData["numScenes"] == 0
assert projData["addNotes"] is False
# Cleanup
nwWiz.reject()
nwWiz.close()
# qtbot.stopForInteraction()
# END Test testToolProjectWizard_Run
+2 -2
View File
@@ -23,8 +23,8 @@ import pytest
import json
import os
from tools import getGuiItem, writeFile
from mock import causeOSError
from tools import getGuiItem, writeFile, buildTestProject
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
@@ -48,7 +48,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
# Create a project to work on
assert nwGUI.newProject({"projPath": fncProj})
buildTestProject(nwGUI, fncProj)
qtbot.wait(100)
assert nwGUI.saveProject()
sessFile = os.path.join(fncProj, "meta", nwFiles.SESS_STATS)
+56
View File
@@ -20,10 +20,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import time
import shutil
from PyQt5.QtWidgets import qApp
XML_IGNORE = ("<novelWriterXML", "<saveCount", "<autoCount", "<editTime")
def cmpFiles(fileOne, fileTwo, ignoreLines=None, ignoreStart=None):
"""Compare two files, but optionally ignore lines given by a list.
@@ -119,3 +122,56 @@ def cleanProject(projPath):
os.unlink(tocFile)
return
def buildTestProject(theObject, projPath):
"""Build a standard test project in projPath using theProject
object as the parent.
"""
from novelwriter.enum import nwItemClass
from novelwriter.core import NWProject, NWDoc
if isinstance(theObject, NWProject):
theGUI = None
theProject = theObject
else:
theGUI = theObject
theProject = theObject.theProject
theProject.clearProject()
theProject.setProjectPath(projPath, newProject=True)
theProject.setProjectName("New Project")
theProject.setBookTitle("New Novel")
theProject.setBookAuthors("Jane Doe")
# Creating a minimal project with a few root folders and a
# single chapter folder with a single file.
xHandle = {}
xHandle[1] = theProject.newRoot(nwItemClass.NOVEL, "Novel")
xHandle[2] = theProject.newRoot(nwItemClass.PLOT, "Plot")
xHandle[3] = theProject.newRoot(nwItemClass.CHARACTER, "Characters")
xHandle[4] = theProject.newRoot(nwItemClass.WORLD, "World")
xHandle[5] = theProject.newFile("Title Page", xHandle[1])
xHandle[6] = theProject.newFolder("New Chapter", xHandle[1])
xHandle[7] = theProject.newFile("New Chapter", xHandle[6])
xHandle[8] = theProject.newFile("New Scene", xHandle[6])
aDoc = NWDoc(theProject, xHandle[5])
aDoc.writeDocument("#! New Novel\n\n>> By Jane DOe <<\n")
aDoc = NWDoc(theProject, xHandle[7])
aDoc.writeDocument("## %s\n\n" % theProject.tr("New Chapter"))
aDoc = NWDoc(theProject, xHandle[8])
aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene"))
theProject.projOpened = time.time()
theProject.setProjectChanged(True)
theProject.saveProject(autoSave=True)
if theGUI is not None:
theGUI.hasProject = True
theGUI.rebuildTrees()
theGUI.rebuildIndex(beQuiet=True)
return