diff --git a/.github/workflows/pytest_cov.yml b/.github/workflows/pytest_cov.yml new file mode 100644 index 00000000..8e60f72b --- /dev/null +++ b/.github/workflows/pytest_cov.yml @@ -0,0 +1,58 @@ +name: PyTest + +on: + pull_request: + branches: [ main, dev ] + +jobs: + pyTestCov: + runs-on: ubuntu-latest + steps: + - name: Python Setup + uses: actions/setup-python@v1 + with: + python-version: 3.8 + architecture: x64 + - name: Install Packages + run: sudo apt install xvfb libenchant-dev qt5-default + - name: Checkout Source + uses: actions/checkout@v2 + - name: Install Dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt + pip install PyVirtualDisplay + pip install pytest-cov + pip install pytest-xvfb + pip install pytest-qt + pip install codecov + - name: Run Tests + run: xvfb-run pytest -v --cov=nw + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + + pyTest: + needs: pyTestCov + strategy: + matrix: + python-version: [3.6, 3.7] + runs-on: ubuntu-latest + steps: + - name: Python Setup + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Install Packages + run: sudo apt install xvfb libenchant-dev qt5-default + - name: Checkout Source + uses: actions/checkout@v2 + - name: Install Dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt + pip install PyVirtualDisplay + pip install pytest-xvfb + pip install pytest-qt + - name: Run Tests + run: xvfb-run pytest -v diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index c12279af..20736e91 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -2,9 +2,9 @@ name: Flake8 Checks on: push: - branches: [ main ] + branches: [ main, dev ] pull_request: - branches: [ main ] + branches: [ main, dev ] jobs: checkSyntax: @@ -15,13 +15,15 @@ jobs: with: python-version: 3.7 architecture: x64 - - name: Checkout novelWriter + - name: Checkout Source uses: actions/checkout@v2 - name: Install flake8 run: pip install flake8 - - name: Check for Syntax Error in novelWriter - run: flake8 nw --count --select=E9,F63,F7,F82 --show-source --statistics - - name: Check for Syntax Error in Tests - run: flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics - - name: Check for Code Style Violations - run: flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics + - name: Syntax Error Check + run: | + flake8 nw --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics + - name: Coding Style Violations + run: | + flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics + flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b0b45ee9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -os: linux -dist: focal -services: - - xvfb -language: python -cache: - - bundler - -addons: - apt: - packages: - - libenchant-dev - - python3-pyqt5 - - python3-pyqt5.qtsvg - - python3-lxml - -install: -# - pip install --upgrade pip - - pip install -r requirements.txt - - pip install PyVirtualDisplay - - pip install pytest-cov - - pip install pytest-xvfb - - pip install pytest-qt - - pip install codecov - -stages: - - name: Main - - name: Supported -# - name: Future -# if: branch = main - -jobs: - include: - - stage: Main - python: 3.8 - script: - - python -m pytest --cov=nw -v - after_success: - - codecov - after_failure: - - cat /sys/fs/cgroup/memory/memory.max_usage_in_bytes - - - stage: Supported - python: 3.6 - script: - - python -m pytest -v - - - python: 3.7 - script: - - python -m pytest -v - -# - stage: -# - Future -# python: -# - 3.9-dev -# script: -# - python -m pytest -v diff --git a/README.md b/README.md index 3c60e951..7adc676f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # novelWriter -[![Build Status](https://travis-ci.com/vkbo/novelWriter.svg?branch=main)](https://travis-ci.com/vkbo/novelWriter) +[![PyTest + Coverage](https://github.com/vkbo/novelWriter/workflows/PyTest%20+%20Coverage/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions) [![codecov](https://codecov.io/gh/vkbo/novelWriter/branch/main/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter) [![Documentation Status](https://readthedocs.org/projects/novelwriter/badge/?version=latest)](https://novelwriter.readthedocs.io/en/latest/?badge=latest) [![Flake8 Checks](https://github.com/vkbo/novelWriter/workflows/Flake8%20Checks/badge.svg)](https://github.com/vkbo/novelWriter/actions) diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py index 9e53653f..6360a321 100644 --- a/nw/gui/outlinedetails.py +++ b/nw/gui/outlinedetails.py @@ -249,8 +249,8 @@ class GuiOutlineDetails(QScrollArea): self.itemValue.setText(nwItem.itemStatus) self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0))) - self.wCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 0))) - self.pCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0))) + self.wCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0))) + self.pCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 0))) self.synopValue.setText(novIdx["synopsis"]) diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index ebfbde69..d6a58a7a 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -48,7 +48,7 @@ True - Lorem Ipusm + Lorem Ipsum FILE NOVEL Finished diff --git a/tests/test_gui.py b/tests/test_gui.py index d6ac126b..f7104308 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -8,14 +8,17 @@ import json from nwtools import cmpFiles from os import path -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QAction +from PyQt5.QtCore import Qt, QPoint +from PyQt5.QtWidgets import QAction, QDialogButtonBox, QTreeWidgetItem from nw.gui import ( GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel, - GuiDocMerge, GuiDocSplit, GuiWritingStats, GuiProjectWizard + GuiDocMerge, GuiDocSplit, GuiWritingStats, GuiProjectWizard, + GuiProjectLoad +) +from nw.constants import ( + nwItemType, nwItemLayout, nwItemClass, nwDocAction, nwUnicode, nwOutline ) -from nw.constants import nwItemType, nwItemLayout, nwItemClass, nwDocAction, nwUnicode keyDelay = 2 stepDelay = 20 @@ -294,6 +297,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): projEdit.show() qtbot.addWidget(projEdit) + qtbot.wait(stepDelay) projEdit.tabMain.editName.setText("") for c in "Project Name": qtbot.keyClick(projEdit.tabMain.editName, c, delay=keyDelay) @@ -306,6 +310,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=keyDelay) # Test Status Tab + qtbot.wait(stepDelay) projEdit._tabBox.setCurrentWidget(projEdit.tabStatus) projEdit.tabStatus.listBox.item(2).setSelected(True) qtbot.mouseClick(projEdit.tabStatus.delButton, Qt.LeftButton) @@ -318,6 +323,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.mouseClick(projEdit.tabStatus.saveButton, Qt.LeftButton) # Auto-Replace Tab + qtbot.wait(stepDelay) projEdit._tabBox.setCurrentWidget(projEdit.tabReplace) qtbot.mouseClick(projEdit.tabReplace.addButton, Qt.LeftButton) @@ -328,19 +334,22 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=keyDelay) qtbot.mouseClick(projEdit.tabReplace.saveButton, Qt.LeftButton) + qtbot.wait(stepDelay) projEdit.tabReplace.listBox.clearSelection() qtbot.mouseClick(projEdit.tabReplace.addButton, Qt.LeftButton) - projEdit.tabReplace.listBox.topLevelItem(0).setSelected(True) - for c in "Delete": - qtbot.keyClick(projEdit.tabReplace.editKey, c, delay=keyDelay) - for c in "This Stuff": - qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=keyDelay) - qtbot.mouseClick(projEdit.tabReplace.saveButton, Qt.LeftButton) - projEdit.tabReplace.listBox.clearSelection() - projEdit.tabReplace.listBox.topLevelItem(0).setSelected(True) + newIdx = -1 + for i in range(projEdit.tabReplace.listBox.topLevelItemCount()): + if projEdit.tabReplace.listBox.topLevelItem(i).text(0) == "": + newIdx = i + break + + assert newIdx >= 0 + newItem = projEdit.tabReplace.listBox.topLevelItem(newIdx) + projEdit.tabReplace.listBox.setCurrentItem(newItem) qtbot.mouseClick(projEdit.tabReplace.delButton, Qt.LeftButton) + qtbot.wait(stepDelay) projEdit._doSave() # Open again, and check project settings @@ -363,8 +372,8 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): projFile = path.join(nwTempGUI, "nwProject.nwx") assert cmpFiles(projFile, path.join(nwRef, "gui", "2_nwProject.nwx"), [2, 8, 9, 10]) - nwGUI.closeMain() # qtbot.stopForInteraction() + nwGUI.closeMain() @pytest.mark.gui def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): @@ -1064,3 +1073,109 @@ def testInsertMenu(qtbot, nwTempGUI, nwFuncTemp, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + +@pytest.mark.gui +def testLoadProject(qtbot, nwTempGUI, nwTemp): + nwGUI = nw.main(["--testmode", "--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwLoad = GuiProjectLoad(nwGUI) + nwLoad.show() + + recentCount = nwLoad.listBox.topLevelItemCount() + assert recentCount > 1 + + selItem = nwLoad.listBox.topLevelItem(1) + selPath = selItem.data(nwLoad.C_NAME, Qt.UserRole) + + nwLoad.selPath.setText("") + nwLoad.listBox.setCurrentItem(selItem) + assert nwLoad.selPath.text() == selPath + + qtbot.mouseClick(nwLoad.buttonBox.button(QDialogButtonBox.Open), Qt.LeftButton) + assert nwLoad.openPath == selPath + assert nwLoad.openState == nwLoad.OPEN_STATE + + del nwLoad + nwLoad = GuiProjectLoad(nwGUI) + nwLoad.show() + + qtbot.mouseClick(nwLoad.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton) + assert nwLoad.openPath is None + assert nwLoad.openState == nwLoad.NONE_STATE + + nwLoad.show() + qtbot.mouseClick(nwLoad.newButton, Qt.LeftButton) + assert nwLoad.openPath is None + assert nwLoad.openState == nwLoad.NEW_STATE + + nwLoad.show() + nwLoad._keyPressDelete() + assert nwLoad.listBox.topLevelItemCount() == recentCount - 1 + + nwLoad.close() + # qtbot.stopForInteraction() + nwGUI.closeMain() + +@pytest.mark.gui +def testOutline(qtbot, nwTempBuild, nwLipsum, nwTemp): + + nwGUI = nw.main(["--testmode", "--config=%s" % nwTempBuild, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + assert nwGUI.openProject(nwLipsum) + nwGUI.mainConf.lastPath = nwTempBuild + + nwGUI.rebuildIndex() + nwGUI.tabWidget.setCurrentIndex(nwGUI.idxTabProj) + + assert nwGUI.projView.topLevelItemCount() > 0 + + # 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) + + nwGUI.projView._loadHeaderState() + assert not nwGUI.projView.colHidden[nwOutline.CCOUNT] + + # First Item + nwGUI.rebuildOutline() + selItem = nwGUI.projView.topLevelItem(0) + assert isinstance(selItem, QTreeWidgetItem) + + nwGUI.projView.setCurrentItem(selItem) + assert nwGUI.projMeta.titleLabel.text() == "Title" + assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum" + assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum" + assert nwGUI.projMeta.itemValue.text() == "Finished" + + assert nwGUI.projMeta.cCValue.text() == "122" + assert nwGUI.projMeta.wCValue.text() == "18" + assert nwGUI.projMeta.pCValue.text() == "2" + + # Scene One + actItem = nwGUI.projView.topLevelItem(1) + chpItem = actItem.child(0) + selItem = chpItem.child(0) + + nwGUI.projView.setCurrentItem(selItem) + assert nwGUI.projMeta.titleLabel.text() == "Scene" + assert nwGUI.projMeta.titleValue.text() == "Scene One" + assert nwGUI.projMeta.fileValue.text() == "Scene One" + assert nwGUI.projMeta.itemValue.text() == "Finished" + + # Click POV Link + assert nwGUI.projMeta.povKeyValue.text() == "Bod" + nwGUI.projMeta._tagClicked("#pov=Bod") + assert nwGUI.docViewer.theHandle == "4c4f28287af27" + + # qtbot.stopForInteraction() + nwGUI.closeMain()