@@ -0,0 +1,623 @@
"""
novelWriter – Item Model Tester
===============================
This file is a part of novelWriter
Copyright 2018– 2024, 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/>.
"""
from __future__ import annotations
import pytest
from PyQt5 . QtCore import QMimeData , QModelIndex , Qt
from novelwriter . common import decodeMimeHandles
from novelwriter . constants import nwConst
from novelwriter . core . item import NWItem
from novelwriter . core . itemmodel import INV_ROOT , NODE_FLAGS , ProjectNode
from novelwriter . core . project import NWProject
from novelwriter . enum import nwItemLayout , nwItemType
from tests . tools import buildTestProject
@pytest.mark.core
def testCoreItemModel_ProjectNode_Root ( mockGUI ) :
""" Test the project node class for the root. """
project = NWProject ( )
root = ProjectNode ( NWItem ( project , INV_ROOT ) )
# Defaults
assert bool ( root ) is True
assert root . item . itemHandle == INV_ROOT
assert repr ( root ) == " <ProjectNode handle=invisibleRoot parent=None row=0 children=0> "
assert root . children == [ ]
assert root . count == 0
# Data
assert root . row ( ) == 0
assert root . childCount ( ) == 0
assert root . parent ( ) is None
assert root . child ( 0 ) is None
assert root . allChildren ( ) == [ ]
@pytest.mark.core
def testCoreItemModel_ProjectNode_Children ( mockGUI , mockRnd , fncPath ) :
""" Test the project node class for children. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
root = project . tree . model . root
# Check Root
assert root . childCount ( ) == 4
assert root . count == 9
# Check Children
child0 = root . child ( 0 )
child1 = root . child ( 1 )
child2 = root . child ( 2 )
child3 = root . child ( 3 )
assert child0 is not None
assert child1 is not None
assert child2 is not None
assert child3 is not None
assert child0 . item . itemName == " Novel "
assert child1 . item . itemName == " Plot "
assert child2 . item . itemName == " Characters "
assert child3 . item . itemName == " Locations "
assert child0 . childCount ( ) == 2
assert child1 . childCount ( ) == 0
assert child2 . childCount ( ) == 0
assert child3 . childCount ( ) == 0
# Check Novel Content
child00 = child0 . child ( 0 )
child01 = child0 . child ( 1 )
assert child00 is not None
assert child01 is not None
assert child00 . item . itemName == " Title Page "
assert child01 . item . itemName == " New Folder "
child010 = child01 . child ( 0 )
child011 = child01 . child ( 1 )
assert child010 is not None
assert child011 is not None
assert child010 . item . itemName == " New Chapter "
assert child011 . item . itemName == " New Scene "
# Check Relationships
assert child0 . parent ( ) is root
assert child1 . parent ( ) is root
assert child2 . parent ( ) is root
assert child3 . parent ( ) is root
assert child01 . parent ( ) is child0
assert child010 . parent ( ) is child01
assert child011 . parent ( ) is child01
@pytest.mark.core
def testCoreItemModel_ProjectNode_Modify ( mockGUI , mockRnd , fncPath ) :
""" Test modifying project nodes. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
root = project . tree . model . root
# Novel folder
novel = root . child ( 0 )
assert novel is not None
assert novel . item . itemName == " Novel "
# Chapter folder
folder = novel . child ( 1 )
assert folder is not None
assert folder . item . itemName == " New Folder "
# Append scene
project . tree . create ( " Scene 1 " , folder . item . itemHandle , nwItemType . FILE )
scene1 = folder . child ( 2 )
assert scene1 is not None
assert scene1 . item . itemName == " Scene 1 "
# Insert scene
project . tree . create ( " Scene 2 " , folder . item . itemHandle , nwItemType . FILE , pos = 1 )
scene2 = folder . child ( 1 )
assert scene2 is not None
assert scene2 . item . itemName == " Scene 2 "
# Scene 1 should now have moved
assert scene1 . row ( ) == 3
assert [ n . item . itemName for n in folder . children ] == [
" New Chapter " , " Scene 2 " , " New Scene " , " Scene 1 " ,
]
# Check that defaults have been set
assert scene1 . item . itemParent == folder . item . itemHandle
assert scene2 . item . itemParent == folder . item . itemHandle
assert scene1 . item . itemClass == folder . item . itemClass
assert scene2 . item . itemClass == folder . item . itemClass
assert scene1 . item . itemLayout == nwItemLayout . DOCUMENT
assert scene2 . item . itemLayout == nwItemLayout . DOCUMENT
# Move Scene 2, invalid position is ignored
folder . moveChild ( 1 , - 1 )
scene2 = folder . child ( 1 )
assert scene2 is not None
assert scene2 . item . itemName == " Scene 2 "
# Move Scene 2, past end is ignored
folder . moveChild ( 1 , 20 )
scene2 = folder . child ( 1 )
assert scene2 is not None
assert scene2 . item . itemName == " Scene 2 "
# Move Scene 2, last position is ok
folder . moveChild ( 1 , 3 )
scene2 = folder . child ( 3 )
assert scene2 is not None
assert scene2 . item . itemName == " Scene 2 "
assert [ n . item . itemName for n in folder . children ] == [
" New Chapter " , " New Scene " , " Scene 1 " , " Scene 2 " ,
]
# Remove original scene, invalid position
removed = folder . takeChild ( - 1 )
assert removed is None
assert folder . childCount ( ) == 4
# Remove original scene, past end
removed = folder . takeChild ( 20 )
assert removed is None
assert folder . childCount ( ) == 4
# Remove original scene, ok
removed = folder . takeChild ( 1 )
assert removed is not None
assert removed . item . itemName == " New Scene "
assert folder . childCount ( ) == 3
assert [ n . item . itemName for n in folder . children ] == [
" New Chapter " , " Scene 1 " , " Scene 2 " ,
]
@pytest.mark.core
def testCoreItemModel_ProjectNode_Data ( mockGUI , mockRnd , fncPath ) :
""" Test data access from project nodes. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
root = project . tree . model . root
# Novel folder
novel = root . child ( 0 )
assert novel is not None
assert novel . item . itemName == " Novel "
# Chapter folder
folder = novel . child ( 1 )
assert folder is not None
assert folder . item . itemName == " New Folder "
# Scene document
scene = folder . child ( 1 )
assert scene is not None
assert scene . item . itemName == " New Scene "
# Check Data
assert novel . data ( 0 , Qt . ItemDataRole . DisplayRole ) == " Novel "
assert novel . data ( 1 , Qt . ItemDataRole . DisplayRole ) == " 9 "
assert scene . data ( 0 , Qt . ItemDataRole . DisplayRole ) == " New Scene "
assert scene . data ( 1 , Qt . ItemDataRole . DisplayRole ) == " 2 "
assert novel . data ( 2 , Qt . ItemDataRole . ToolTipRole ) == " "
assert novel . data ( 3 , Qt . ItemDataRole . ToolTipRole ) == " New "
assert scene . data ( 2 , Qt . ItemDataRole . ToolTipRole ) == " Active "
assert scene . data ( 3 , Qt . ItemDataRole . ToolTipRole ) == " New "
# Check Flags
assert novel . flags ( ) == NODE_FLAGS
assert scene . flags ( ) == NODE_FLAGS | Qt . ItemFlag . ItemIsDragEnabled
@pytest.mark.core
def testCoreItemModel_ProjectModel_Interface ( mockGUI , mockRnd , fncPath ) :
""" Test the model interface for the project model. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
model = project . tree . model
# Init
assert isinstance ( model . root , ProjectNode )
assert model . root . item . itemHandle == INV_ROOT
# Indices
rootIdx = QModelIndex ( )
novelIdx = model . index ( 0 , 0 , rootIdx )
folderIdx = model . index ( 1 , 0 , novelIdx )
sceneIdx = model . index ( 1 , 0 , folderIdx )
invalidIdx = model . index ( - 1 , - 1 )
assert rootIdx . isValid ( ) is False
assert novelIdx . isValid ( ) is True
assert folderIdx . isValid ( ) is True
assert sceneIdx . isValid ( ) is True
assert invalidIdx . isValid ( ) is False
# Columns and Rows
assert model . rowCount ( rootIdx ) == 4
assert model . columnCount ( rootIdx ) == 4
assert model . rowCount ( novelIdx ) == 2
assert model . columnCount ( novelIdx ) == 4
# Parent of Novel
parent = model . parent ( novelIdx )
assert parent . row ( ) == 0
assert parent . column ( ) == 0
assert parent . internalPointer ( ) is model . root
# Parent of Root
parent = model . parent ( rootIdx )
assert parent . isValid ( ) is False
# Data and Flags
assert model . data ( novelIdx , Qt . ItemDataRole . DisplayRole ) == " Novel "
assert model . data ( sceneIdx , Qt . ItemDataRole . DisplayRole ) == " New Scene "
assert model . data ( invalidIdx , Qt . ItemDataRole . DisplayRole ) is None
assert model . flags ( novelIdx ) == NODE_FLAGS
assert model . flags ( sceneIdx ) == NODE_FLAGS | Qt . ItemFlag . ItemIsDragEnabled
assert model . flags ( invalidIdx ) == Qt . ItemFlag . NoItemFlags
@pytest.mark.core
def testCoreItemModel_ProjectModel_DragNDrop ( mockGUI , mockRnd , fncPath ) :
""" Test drag and drop for the project model. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
model = project . tree . model
# Nodes
novel = model . root . child ( 0 )
assert novel is not None
folder = novel . child ( 1 )
assert folder is not None
chapter = folder . child ( 0 )
assert chapter is not None
scene = folder . child ( 1 )
assert scene is not None
assert novel . item . itemName == " Novel "
assert folder . item . itemName == " New Folder "
assert chapter . item . itemName == " New Chapter "
assert scene . item . itemName == " New Scene "
# Indices
rootIdx = QModelIndex ( )
novelIdx = model . index ( 0 , 0 , rootIdx )
folderIdx = model . index ( 1 , 0 , novelIdx )
chapterIdx = model . index ( 0 , 0 , folderIdx )
sceneIdx = model . index ( 1 , 0 , folderIdx )
invalidIdx = model . index ( - 1 , - 1 )
# Only move is allowed
assert model . supportedDragActions ( ) == Qt . DropAction . MoveAction
assert model . supportedDropActions ( ) == Qt . DropAction . MoveAction
# Only handles are dragged and dropped
assert model . mimeTypes ( ) == [ nwConst . MIME_HANDLE ]
# Get mime data
novelMime = model . mimeData ( [ novelIdx ] )
assert decodeMimeHandles ( novelMime ) == [ novel . item . itemHandle ]
sceneMime = model . mimeData ( [ sceneIdx ] )
assert decodeMimeHandles ( sceneMime ) == [ scene . item . itemHandle ]
sceneChapterMime = model . mimeData ( [ chapterIdx , sceneIdx ] )
assert decodeMimeHandles ( sceneChapterMime ) == [
chapter . item . itemHandle , scene . item . itemHandle ,
]
multiMime = model . mimeData ( [ novelIdx , folderIdx , sceneIdx , invalidIdx ] )
assert decodeMimeHandles ( multiMime ) == [
novel . item . itemHandle , folder . item . itemHandle , scene . item . itemHandle ,
]
# Check that drop is possible
invalidMime = QMimeData ( )
invalidMime . setData ( " plain/text " , b " foobar " )
assert model . canDropMimeData ( invalidMime , Qt . DropAction . MoveAction , 0 , 0 , novelIdx ) is False
assert model . canDropMimeData ( sceneMime , Qt . DropAction . MoveAction , 0 , 0 , novelIdx ) is True
# Drop the scene on the novel folder
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " Title Page " , " New Folder " , " New Chapter " , " New Scene " ,
" Plot " , " Characters " , " Locations " ,
]
assert model . dropMimeData ( invalidMime , Qt . DropAction . MoveAction , 0 , 0 , novelIdx ) is False
assert model . dropMimeData ( sceneChapterMime , Qt . DropAction . MoveAction , 0 , 0 , novelIdx ) is True
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " New Chapter " , " New Scene " , " Title Page " , " New Folder " ,
" Plot " , " Characters " , " Locations " ,
]
@pytest.mark.core
def testCoreItemModel_ProjectModel_Data ( mockGUI , mockRnd , fncPath ) :
""" Test data access for the project model. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
model = project . tree . model
# Nodes
root = model . root
novel = root . child ( 0 )
assert novel is not None
folder = novel . child ( 1 )
assert folder is not None
chapter = folder . child ( 0 )
assert chapter is not None
scene = folder . child ( 1 )
assert scene is not None
assert novel . item . itemName == " Novel "
assert folder . item . itemName == " New Folder "
assert chapter . item . itemName == " New Chapter "
assert scene . item . itemName == " New Scene "
# Indices
rootIdx = QModelIndex ( )
novelIdx = model . index ( 0 , 0 , rootIdx )
folderIdx = model . index ( 1 , 0 , novelIdx )
chapterIdx = model . index ( 0 , 0 , folderIdx )
sceneIdx = model . index ( 1 , 0 , folderIdx )
invalidIdx = model . index ( - 1 , - 1 )
# Check Rows
assert model . row ( rootIdx ) == - 1
assert model . row ( novelIdx ) == 0
assert model . row ( folderIdx ) == 1
assert model . row ( chapterIdx ) == 0
assert model . row ( sceneIdx ) == 1
assert model . row ( invalidIdx ) == - 1
# Check Nodes
assert model . node ( rootIdx ) is None
assert model . node ( novelIdx ) is novel
assert model . node ( folderIdx ) is folder
assert model . node ( chapterIdx ) is chapter
assert model . node ( sceneIdx ) is scene
assert model . node ( invalidIdx ) is None
nodes = model . nodes ( [ novelIdx , folderIdx , chapterIdx , sceneIdx ] )
assert nodes [ 0 ] is novel
assert nodes [ 1 ] is folder
assert nodes [ 2 ] is chapter
assert nodes [ 3 ] is scene
# Index from Handle
assert model . indexFromHandle ( None ) . isValid ( ) is False
assert model . node ( model . indexFromHandle ( novel . item . itemHandle ) ) is novel
assert model . node ( model . indexFromHandle ( folder . item . itemHandle ) ) is folder
assert model . node ( model . indexFromHandle ( chapter . item . itemHandle ) ) is chapter
assert model . node ( model . indexFromHandle ( scene . item . itemHandle ) ) is scene
# Index from Node
assert model . indexFromHandle ( None ) . isValid ( ) is False
assert model . node ( model . indexFromNode ( novel ) ) is novel
assert model . node ( model . indexFromNode ( folder ) ) is folder
assert model . node ( model . indexFromNode ( chapter ) ) is chapter
assert model . node ( model . indexFromNode ( scene ) ) is scene
@pytest.mark.core
def testCoreItemModel_ProjectModel_Edit ( qtbot , mockGUI , mockRnd , fncPath ) :
""" Test editing the project model. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
model = project . tree . model
# Nodes
root = model . root
novel = root . child ( 0 )
assert novel is not None
title = novel . child ( 0 )
assert title is not None
folder = novel . child ( 1 )
assert folder is not None
chapter = folder . child ( 0 )
assert chapter is not None
scene = folder . child ( 1 )
assert scene is not None
assert novel . item . itemName == " Novel "
assert title . item . itemName == " Title Page "
assert folder . item . itemName == " New Folder "
assert chapter . item . itemName == " New Chapter "
assert scene . item . itemName == " New Scene "
# Indices
novelIdx = model . indexFromNode ( novel )
titleIdx = model . indexFromNode ( title )
folderIdx = model . indexFromNode ( folder )
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
invalidIdx = model . index ( - 1 , - 1 )
# Initial Order
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " Title Page " , " New Folder " , " New Chapter " , " New Scene " ,
" Plot " , " Characters " , " Locations " ,
]
# Remove Child, invalid index
assert model . removeChild ( novelIdx , - 1 ) is None
assert model . removeChild ( novelIdx , 99 ) is None
# Remove Child
with qtbot . waitSignal ( model . rowsAboutToBeRemoved ) as signal :
child = model . removeChild ( novelIdx , titleIdx . row ( ) )
assert signal . args [ 0 ] . internalPointer ( ) . item . itemName == " Novel "
assert signal . args [ 1 ] == 0
assert signal . args [ 2 ] == 0
assert child is not None
assert child . item . itemName == " Title Page "
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " New Folder " , " New Chapter " , " New Scene " ,
" Plot " , " Characters " , " Locations " ,
]
titleIdx = model . indexFromNode ( title )
folderIdx = model . indexFromNode ( folder )
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
# Insert Child
with qtbot . waitSignal ( model . rowsAboutToBeInserted ) as signal :
model . insertChild ( child , novelIdx , 1 )
assert signal . args [ 0 ] . internalPointer ( ) . item . itemName == " Novel "
assert signal . args [ 1 ] == 1
assert signal . args [ 2 ] == 1
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " New Folder " , " New Chapter " , " New Scene " , " Title Page " ,
" Plot " , " Characters " , " Locations " ,
]
titleIdx = model . indexFromNode ( title )
folderIdx = model . indexFromNode ( folder )
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
# Move it back with internal move
with qtbot . waitSignal ( model . rowsAboutToBeMoved ) as signal :
model . internalMove ( titleIdx , - 1 )
assert signal . args [ 0 ] . internalPointer ( ) . item . itemName == " Novel "
assert signal . args [ 1 ] == 1
assert signal . args [ 2 ] == 1
assert signal . args [ 3 ] . internalPointer ( ) . item . itemName == " Novel "
assert signal . args [ 4 ] == 0
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " Title Page " , " New Folder " , " New Chapter " , " New Scene " ,
" Plot " , " Characters " , " Locations " ,
]
titleIdx = model . indexFromNode ( title )
folderIdx = model . indexFromNode ( folder )
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
# Move Multiple, with parent
# Chapter and scene selection is flipped, but they should be deselected
# because folder is also selected, so their order should not change
model . multiMove ( [ folderIdx , sceneIdx , chapterIdx , invalidIdx ] , novelIdx , 0 )
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " New Folder " , " New Chapter " , " New Scene " , " Title Page " ,
" Plot " , " Characters " , " Locations " ,
]
assert folder . parent ( ) is novel
assert chapter . parent ( ) is folder
assert scene . parent ( ) is folder
titleIdx = model . indexFromNode ( title )
folderIdx = model . indexFromNode ( folder )
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
# Move Multiple, siblings, altered order
# Chapter and scene selection is flipped, and they should now be reordered,
# and no longer in the folder
model . multiMove ( [ sceneIdx , chapterIdx , invalidIdx ] , novelIdx , 0 )
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " New Scene " , " New Chapter " , " New Folder " , " Title Page " ,
" Plot " , " Characters " , " Locations " ,
]
assert folder . parent ( ) is novel
assert chapter . parent ( ) is novel
assert scene . parent ( ) is novel
@pytest.mark.core
def testCoreItemModel_ProjectModel_Other ( qtbot , mockGUI , mockRnd , fncPath ) :
""" Test other methods of the project model. """
project = NWProject ( )
mockRnd . reset ( )
buildTestProject ( project , fncPath )
model = project . tree . model
# Nodes
root = model . root
novel = root . child ( 0 )
assert novel is not None
title = novel . child ( 0 )
assert title is not None
folder = novel . child ( 1 )
assert folder is not None
chapter = folder . child ( 0 )
assert chapter is not None
scene = folder . child ( 1 )
assert scene is not None
trash = project . tree . trash
assert trash is not None
assert novel . item . itemName == " Novel "
assert title . item . itemName == " Title Page "
assert folder . item . itemName == " New Folder "
assert chapter . item . itemName == " New Chapter "
assert scene . item . itemName == " New Scene "
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " Title Page " , " New Folder " , " New Chapter " , " New Scene " ,
" Plot " , " Characters " , " Locations " , " Trash " ,
]
# Indices
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
trashIdx = model . indexFromNode ( trash )
# Expanded
assert model . allExpanded ( ) == [ ]
novel . item . setExpanded ( True )
folder . item . setExpanded ( True )
assert [ model . node ( i ) for i in model . allExpanded ( ) ] == [ novel , folder ]
# Check Trash
assert model . trashSelection ( [ chapterIdx , sceneIdx ] ) is False
model . multiMove ( [ chapterIdx , sceneIdx ] , trashIdx )
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [
" Novel " , " Title Page " , " New Folder " ,
" Plot " , " Characters " , " Locations " , " Trash " , " New Chapter " , " New Scene " ,
]
chapterIdx = model . indexFromNode ( chapter )
sceneIdx = model . indexFromNode ( scene )
assert model . trashSelection ( [ chapterIdx , sceneIdx ] ) is True
# Clear
model . clear ( )
assert [ n . item . itemName for n in model . root . allChildren ( ) ] == [ ]