From f8f3b61452f7c5eeba060eea7d7caa8bf46d9378 Mon Sep 17 00:00:00 2001 From: Minoru Akagi Date: Thu, 2 Nov 2023 15:34:04 +0900 Subject: [PATCH] add some GUI tests --- q3dconst.py | 4 +- q3dwebbridge.py | 5 + q3dwindow.py | 57 +-- tests/data/pt4.csv | 2 +- ...qgs.qto3settings => scene1_1.qto3settings} | 232 ++++++++++-- tests/gui/__init__.py | 0 tests/gui/test.js | 144 +++++++ tests/gui/test_gui.py | 353 ++++++++++++++++++ tests/gui/wnd_geom.bin | Bin 0 -> 66 bytes tests/test_basic.py | 4 +- tests/utilities.py | 53 +-- viewer/viewer.css | 10 + 12 files changed, 776 insertions(+), 88 deletions(-) rename tests/data/{testproject1.qgs.qto3settings => scene1_1.qto3settings} (90%) create mode 100644 tests/gui/__init__.py create mode 100644 tests/gui/test.js create mode 100644 tests/gui/test_gui.py create mode 100644 tests/gui/wnd_geom.bin diff --git a/q3dconst.py b/q3dconst.py index 5816ba7c..856db748 100644 --- a/q3dconst.py +++ b/q3dconst.py @@ -39,6 +39,7 @@ class Script: VIEWHELPER = 8 MESHLINE = 9 FETCH = 101 + TEST = 201 # Script path (relative from js directory) PATH = { @@ -51,7 +52,8 @@ class Script: OUTLINE: "threejs/effects/OutlineEffect.js", VIEWHELPER: "threejs/editor/ViewHelper.js", MESHLINE: "meshline/THREE.MeshLine.js", - FETCH: "unfetch/unfetch.js" + FETCH: "unfetch/unfetch.js", + TEST: "../tests/gui/test.js" } diff --git a/q3dwebbridge.py b/q3dwebbridge.py index 382d5a59..75e8b009 100644 --- a/q3dwebbridge.py +++ b/q3dwebbridge.py @@ -21,6 +21,7 @@ class Bridge(QObject): imageReady = pyqtSignal(int, int, "QImage") tweenStarted = pyqtSignal(int) animationStopped = pyqtSignal() + testResultReceived = pyqtSignal(str, bool, str) def __init__(self, parent=None): QObject.__init__(self, parent) @@ -75,6 +76,10 @@ def onTweenStarted(self, index): def onAnimationStopped(self): self.animationStopped.emit() + @pyqtSlot(str, bool, str) + def sendTestResult(self, testName, result, msg): + self.testResultReceived.emit(testName, result, msg) + """ @pyqtSlot(int, int, result=str) def mouseUpMessage(self, x, y): diff --git a/q3dwindow.py b/q3dwindow.py index 7c544730..87ee4ad8 100644 --- a/q3dwindow.py +++ b/q3dwindow.py @@ -235,21 +235,23 @@ def setupMenu(self): self.alwaysOnTopToggled(False) if DEBUG_MODE: - self.ui.menuDebug = QMenu(self.ui.menubar) - self.ui.menuDebug.setObjectName("menuDebug") - self.ui.menuDebug.setTitle("&Debug") - self.ui.menubar.addAction(self.ui.menuDebug.menuAction()) + self.ui.menuDev = QMenu(self.ui.menubar) + self.ui.menuDev.setTitle("&Dev") + self.ui.menubar.addAction(self.ui.menuDev.menuAction()) + + self.ui.actionTest = QAction(self) + self.ui.actionTest.setText("Run Test") + self.ui.menuDev.addAction(self.ui.actionTest) + self.ui.actionTest.triggered.connect(self.runTest) self.ui.actionInspector = QAction(self) - self.ui.actionInspector.setObjectName("actionInspector") self.ui.actionInspector.setText("Web Inspector...") - self.ui.menuDebug.addAction(self.ui.actionInspector) + self.ui.menuDev.addAction(self.ui.actionInspector) self.ui.actionInspector.triggered.connect(self.ui.webView.showInspector) self.ui.actionJSInfo = QAction(self) - self.ui.actionJSInfo.setObjectName("actionJSInfo") self.ui.actionJSInfo.setText("three.js Info...") - self.ui.menuDebug.addAction(self.ui.actionJSInfo) + self.ui.menuDev.addAction(self.ui.actionJSInfo) self.ui.actionJSInfo.triggered.connect(self.ui.webView.showJSInfo) def setupConsole(self): @@ -300,6 +302,7 @@ def showLayerPropertiesDialog(self, layer): dialog.propertiesAccepted.connect(self.updateLayerProperties) dialog.showLayerProperties(layer) + return dialog # @pyqtSlot(Layer) def updateLayerProperties(self, layer): @@ -392,13 +395,14 @@ def saveAsGLTF(self): self.ui.statusbar.clearMessage() self.lastDir = os.path.dirname(filename) - def loadSettings(self): - # file open dialog - directory = self.lastDir or QgsProject.instance().homePath() or QDir.homePath() - filterString = "Settings files (*.qto3settings);;All files (*.*)" - filename, _ = QFileDialog.getOpenFileName(self, "Load Export Settings", directory, filterString) + def loadSettings(self, filename=None): + # open file dialog if filename is not specified if not filename: - return + directory = self.lastDir or QgsProject.instance().homePath() or QDir.homePath() + filterString = "Settings files (*.qto3settings);;All files (*.*)" + filename, _ = QFileDialog.getOpenFileName(self, "Load Export Settings", directory, filterString) + if not filename: + return self.ui.treeView.uncheckAll() # hide all 3D objects from the scene self.ui.treeView.clearLayers() @@ -413,16 +417,17 @@ def loadSettings(self): self.lastDir = os.path.dirname(filename) - def saveSettings(self): - # file save dialog - directory = self.lastDir or QgsProject.instance().homePath() or QDir.homePath() - filename, _ = QFileDialog.getSaveFileName(self, "Save Export Settings", directory, "Settings files (*.qto3settings)") + def saveSettings(self, filename=None): + # open file dialog if filename is not specified if not filename: - return + directory = self.lastDir or QgsProject.instance().homePath() or QDir.homePath() + filename, _ = QFileDialog.getSaveFileName(self, "Save Export Settings", directory, "Settings files (*.qto3settings)") + if not filename: + return - # append .qto3settings extension if filename doesn't have - if os.path.splitext(filename)[1].lower() != ".qto3settings": - filename += ".qto3settings" + # append .qto3settings extension if filename doesn't have + if os.path.splitext(filename)[1].lower() != ".qto3settings": + filename += ".qto3settings" self.settings.setAnimationData(self.ui.animationPanel.data()) self.settings.saveSettings(filename) @@ -457,6 +462,7 @@ def showScenePropertiesDialog(self): dialog = PropertiesDialog(self.settings, self.qgisIface, self) dialog.propertiesAccepted.connect(self.updateSceneProperties) dialog.showSceneProperties() + return dialog # @pyqtSlot(dict) def updateSceneProperties(self, properties): @@ -555,6 +561,11 @@ def sendFeedback(self): def about(self): QMessageBox.information(self, "Qgis2threejs Plugin", "Plugin version: {0}".format(PLUGIN_VERSION), QMessageBox.Ok) + # Dev menu + def runTest(self): + from Qgis2threejs.tests.gui.test_gui import runTest + runTest(self) + class PropertiesDialog(QDialog): @@ -619,7 +630,6 @@ def showLayerProperties(self, layer): self.setLayerDialogTitle(layer) self.setLayer(layer) self.show() - self.exec_() def setLayerDialogTitle(self, layer): if layer.mapLayer: @@ -636,7 +646,6 @@ def showSceneProperties(self): self.page = ScenePropertyPage(self, self.settings.sceneProperties(), self.qgisIface.mapCanvas()) self.ui.scrollArea.setWidget(self.page) self.show() - self.exec_() class WheelEventFilter(QObject): diff --git a/tests/data/pt4.csv b/tests/data/pt4.csv index 958233b5..76eca4f2 100644 --- a/tests/data/pt4.csv +++ b/tests/data/pt4.csv @@ -4,4 +4,4 @@ pk,label,r,h,WKT 3,cone 3,500,1000,POINT Z (138.6 35 1000) 4,cone 4,500,1000,POINT Z (138.6 35.1 1500) 5,cone 5,,,POINT Z (138.6 35.2 2000) -6,富士山,250,500,POINT Z (138.7300 35.3624 3776) +6,富士山,250,600,POINT Z (138.7300 35.3624 3776) diff --git a/tests/data/testproject1.qgs.qto3settings b/tests/data/scene1_1.qto3settings similarity index 90% rename from tests/data/testproject1.qgs.qto3settings rename to tests/data/scene1_1.qto3settings index 6b251d82..67304521 100644 --- a/tests/data/testproject1.qgs.qto3settings +++ b/tests/data/scene1_1.qto3settings @@ -235,7 +235,8 @@ "enabled": true, "groups": [] } - } + }, + "repeat": false }, "LAYERS": [ { @@ -244,9 +245,37 @@ "name": "pt1", "properties": { "checkBox_Clickable": true, + "checkBox_Clip": true, "checkBox_ExportAttrs": true, + "checkBox_Label": false, + "checkBox_Outline": true, + "checkBox_Underline": false, "checkBox_Visible": true, - "comboBox_Label": null, + "colorButton_BgColor": [ + 255, + 255, + 255, + 176 + ], + "colorButton_ConnColor": [ + 192, + 192, + 208, + 255 + ], + "colorButton_Label": [ + 0, + 0, + 0, + 255 + ], + "colorButton_OtlColor": [ + 255, + 255, + 255, + 255 + ], + "comboBox_FontFamily": "sans-serif", "comboBox_ObjectType": "Sphere", "comboBox_altitudeMode": null, "comboEdit_Color": { @@ -255,19 +284,34 @@ "editText": "", "type": 2 }, + "comboEdit_Color2": { + "comboData": null, + "comboText": "None", + "editText": "", + "type": 7 + }, + "comboEdit_FilePath": {}, "comboEdit_Opacity": { "comboData": 1, "comboText": "Feature style", "editText": "", "type": 5 }, + "comboEdit_altitude2": {}, + "expression_Label": "\"pk\"", "fieldExpressionWidget_altitude": "0", "geomWidget0": { "comboData": null, "comboText": "Expression", - "editText": "1000", + "editText": "r * 3", "type": 1 }, + "geomWidget1": {}, + "geomWidget2": {}, + "geomWidget3": {}, + "geomWidget4": {}, + "groupBox_Background": true, + "groupBox_Conn": true, "labelHeightWidget": { "comboData": 0, "comboText": "Absolute", @@ -275,8 +319,11 @@ "type": 6 }, "lineEdit_Name": "", + "mtlWidget0": {}, + "mtlWidget1": {}, "radioButton_IntersectingFeatures": true, - "radioButton_zValue": true + "radioButton_zValue": true, + "slider_FontSize": 3 }, "visible": true }, @@ -286,9 +333,38 @@ "name": "pt2", "properties": { "checkBox_Clickable": true, + "checkBox_Clip": true, "checkBox_ExportAttrs": false, + "checkBox_Label": false, + "checkBox_Outline": true, + "checkBox_Underline": false, "checkBox_Visible": true, - "comboBox_ObjectType": "Cylinder", + "colorButton_BgColor": [ + 255, + 255, + 255, + 176 + ], + "colorButton_ConnColor": [ + 192, + 192, + 208, + 255 + ], + "colorButton_Label": [ + 0, + 0, + 0, + 255 + ], + "colorButton_OtlColor": [ + 255, + 255, + 255, + 255 + ], + "comboBox_FontFamily": "sans-serif", + "comboBox_ObjectType": "Box", "comboBox_altitudeMode": null, "comboEdit_Color": { "comboData": 1, @@ -296,28 +372,56 @@ "editText": "", "type": 2 }, + "comboEdit_Color2": { + "comboData": null, + "comboText": "None", + "editText": "", + "type": 7 + }, + "comboEdit_FilePath": {}, "comboEdit_Opacity": { "comboData": 1, "comboText": "Feature style", "editText": "", "type": 5 }, + "comboEdit_altitude2": {}, + "expression_Label": "\"pk\"", "fieldExpressionWidget_altitude": "0", "geomWidget0": { "comboData": null, "comboText": "Expression", - "editText": "1000", + "editText": "w * 5", "type": 1 }, "geomWidget1": { + "comboData": null, + "comboText": "Expression", + "editText": "d * 5", + "type": 1 + }, + "geomWidget2": { "comboData": null, "comboText": "Expression", "editText": "\"h\"", "type": 1 }, + "geomWidget3": {}, + "geomWidget4": {}, + "groupBox_Background": true, + "groupBox_Conn": true, + "labelHeightWidget": { + "comboData": 1, + "comboText": "Relative", + "editText": "50", + "type": 6 + }, "lineEdit_Name": "", + "mtlWidget0": {}, + "mtlWidget1": {}, "radioButton_IntersectingFeatures": true, - "radioButton_zValue": true + "radioButton_zValue": true, + "slider_FontSize": 3 }, "visible": true }, @@ -358,7 +462,7 @@ 255 ], "comboBox_FontFamily": "sans-serif", - "comboBox_ObjectType": "Cone", + "comboBox_ObjectType": "Cylinder", "comboBox_altitudeMode": null, "comboEdit_Color": { "comboData": 3, @@ -385,13 +489,13 @@ "geomWidget0": { "comboData": null, "comboText": "Expression", - "editText": "1000", + "editText": "r * 4", "type": 1 }, "geomWidget1": { "comboData": null, "comboText": "Expression", - "editText": "1000", + "editText": "h * 2", "type": 1 }, "geomWidget2": {}, @@ -420,9 +524,38 @@ "name": "pt4", "properties": { "checkBox_Clickable": true, - "checkBox_ExportAttrs": false, + "checkBox_Clip": true, + "checkBox_ExportAttrs": true, + "checkBox_Label": false, + "checkBox_Outline": true, + "checkBox_Underline": false, "checkBox_Visible": true, - "comboBox_ObjectType": "Box", + "colorButton_BgColor": [ + 255, + 255, + 255, + 176 + ], + "colorButton_ConnColor": [ + 192, + 192, + 208, + 255 + ], + "colorButton_Label": [ + 0, + 0, + 0, + 255 + ], + "colorButton_OtlColor": [ + 255, + 255, + 255, + 255 + ], + "comboBox_FontFamily": "sans-serif", + "comboBox_ObjectType": "Cone", "comboBox_altitudeMode": null, "comboEdit_Color": { "comboData": 1, @@ -430,34 +563,51 @@ "editText": "", "type": 2 }, + "comboEdit_Color2": { + "comboData": null, + "comboText": "None", + "editText": "", + "type": 7 + }, + "comboEdit_FilePath": {}, "comboEdit_Opacity": { "comboData": 1, "comboText": "Feature style", "editText": "", "type": 5 }, + "comboEdit_altitude2": {}, + "expression_Label": "\"pk\"", "fieldExpressionWidget_altitude": "0", "geomWidget0": { "comboData": null, "comboText": "Expression", - "editText": "1000", + "editText": "r * 8", "type": 1 }, "geomWidget1": { "comboData": null, "comboText": "Expression", - "editText": "1000", + "editText": "h * 5", "type": 1 }, - "geomWidget2": { - "comboData": null, - "comboText": "Expression", - "editText": "1000", - "type": 1 + "geomWidget2": {}, + "geomWidget3": {}, + "geomWidget4": {}, + "groupBox_Background": true, + "groupBox_Conn": true, + "labelHeightWidget": { + "comboData": 1, + "comboText": "Relative", + "editText": "50", + "type": 6 }, "lineEdit_Name": "", + "mtlWidget0": {}, + "mtlWidget1": {}, "radioButton_IntersectingFeatures": true, - "radioButton_zValue": true + "radioButton_zValue": true, + "slider_FontSize": 3 }, "visible": true }, @@ -498,8 +648,8 @@ 255 ], "comboBox_FontFamily": "sans-serif", - "comboBox_ObjectType": "Point", - "comboBox_altitudeMode": null, + "comboBox_ObjectType": "Disk", + "comboBox_altitudeMode": "dem_srtm3020150914165149263", "comboEdit_Color": { "comboData": 2, "comboText": "Random", @@ -521,10 +671,25 @@ }, "comboEdit_altitude2": {}, "expression_Label": "\"pk\"", - "fieldExpressionWidget_altitude": "\"r\"", - "geomWidget0": {}, - "geomWidget1": {}, - "geomWidget2": {}, + "fieldExpressionWidget_altitude": "0", + "geomWidget0": { + "comboData": null, + "comboText": "Expression", + "editText": "r * 2", + "type": 1 + }, + "geomWidget1": { + "comboData": null, + "comboText": "Expression", + "editText": "\"dip\"", + "type": 1 + }, + "geomWidget2": { + "comboData": null, + "comboText": "Expression", + "editText": "\"direction\"", + "type": 1 + }, "geomWidget3": {}, "geomWidget4": {}, "groupBox_Background": true, @@ -536,12 +701,7 @@ "type": 6 }, "lineEdit_Name": "", - "mtlWidget0": { - "comboData": null, - "comboText": "Expression", - "editText": "1000", - "type": 1 - }, + "mtlWidget0": {}, "mtlWidget1": {}, "radioButton_Expression": true, "radioButton_IntersectingFeatures": true, @@ -1105,7 +1265,7 @@ { "geomType": 0, "layerId": "fp:89c5f24c", - "name": "Flat Plane (-500)", + "name": "Flat Plane (-4000)", "properties": { "checkBox_Clickable": true, "checkBox_Frame": false, @@ -1131,7 +1291,7 @@ 0, 255 ], - "lineEdit_Altitude": "-500", + "lineEdit_Altitude": "-4000", "lineEdit_Bottom": "0", "lineEdit_Name": "", "materials": [ @@ -1703,11 +1863,11 @@ "slider_Fog": 54 }, "Template": "3DViewer(dat-gui).html", - "Version": 20600, + "Version": 20701, "WIDGETS": { "Label": { "Footer": "", - "Header": "
Test Project
" + "Header": "
Test Scene 1
" }, "NorthArrow": { "color": "0x66e866", diff --git a/tests/gui/__init__.py b/tests/gui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/gui/test.js b/tests/gui/test.js new file mode 100644 index 00000000..49bee6d8 --- /dev/null +++ b/tests/gui/test.js @@ -0,0 +1,144 @@ +// (C) 2023 Minoru Akagi +// SPDX-License-Identifier: MIT +// https://github.com/minorua/Qgis2threejs + +function assertText(testName, text, startingElemId, partialMatch) { + + var compareText = function (elem, text, partialMatch) { + + if (partialMatch) { + + if (elem.innerText.indexOf(text) !== -1) return true; + + } + else { + + if (elem.innerText == text) return true; + + } + + return false; + + }; + + var result = false; + + if (startingElemId) { + + result = compareText(document.getElementById(startingElemId), text, partialMatch); + + } + + if (!result) { + + var elems = document.querySelectorAll((startingElemId) ? "#" + startingElemId + " *" : "*"); + + for (var i = 0, l = elems.length; i < l && !result; i++) { + + result = compareText(elems[i], text, partialMatch); + + } + + } + + var message = text + (result ? "" : " not") + " found"; + if (startingElemId) message += " in element '" + startingElemId + "'"; + + pyObj.sendTestResult(testName, result, message + "."); + +} + +function assertVisibility(testName, elemId, expected) { + + var elem = document.getElementById(elemId), + visible = (window.getComputedStyle(elem).display != "none"); + + pyObj.sendTestResult(testName, (visible == expected), "element '" + elemId + "' is " + (visible) ? "visible." : "invisible."); + +} + +function assertBox3(testName, box1, box2) { + + var msg; + + if (box2 === undefined) { + + box2 = new THREE.Box3().setFromObject(app.scene); + msg = "a box and scene bbox"; + + } + else { + + msg = "two boxes"; + + } + + var result = box1.equals(box2); + + if (result) { + + result = true; + msg += " are same."; + + } + else { + + msg += " are not same."; + + } + + pyObj.sendTestResult(testName, result, msg); + +} + +function assertZRange(testName, obj, min, max) { + + var box = new THREE.Box3().setFromObject(obj); + var result = true, msg = ""; + + if (min !== undefined && min != box.min.z) { + + result = false; + msg += "bottom z is different from expected. (" + box.min.z + ", exptected: " + min + ")" + + } + + if (max !== undefined && max != box.max.z) { + + result = false; + msg += "top z is different from expected. (" + box.max.z + ", exptected: " + max + ")"; + + } + + pyObj.sendTestResult(testName, result, msg); + +} + +var markerElem, markerTimerId; + +function showMarker(x, y, msec) { + if (markerElem) { + markerElem.style.display = "block"; + } + else { + markerElem = document.createElement("div"); + markerElem.id = "testmarker"; + document.getElementById("view").appendChild(markerElem); + } + + var hw = markerElem.offsetWidth / 2; + markerElem.style.left = (x - hw) + "px"; + markerElem.style.top = (y - hw) + "px"; + + if (markerTimerId) { + clearTimeout(markerTimerId); + markerTimerId = 0; + } + if (msec !== undefined) markerTimerId = setTimeout(hideMarker, msec); +} + +function hideMarker() { + if (markerElem) { + markerElem.style.display = "none"; + } +} diff --git a/tests/gui/test_gui.py b/tests/gui/test_gui.py new file mode 100644 index 00000000..731d2e24 --- /dev/null +++ b/tests/gui/test_gui.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +# (C) 2023 Minoru Akagi +# SPDX-License-Identifier: GPL-2.0-or-later +# begin: 2023-10-16 + +import os +from PyQt5.QtCore import Qt, QEventLoop, QPoint, QTimer +from PyQt5.QtWidgets import QDialogButtonBox, QMessageBox +from PyQt5.QtTest import QTest +from qgis.core import QgsProject, QgsRectangle +from qgis.testing import unittest + +from Qgis2threejs.q3dconst import Script +from Qgis2threejs.tests.utilities import dataPath, initOutputDir +from Qgis2threejs.tools import js_bool, logMessage + + +UNDEF = "undefined" + + +def Box3(min, max): + """min/max: a list containing three coordinate values (x, y, z)""" + return "new THREE.Box3({}, {})".format(Vec3(*min), Vec3(*max)) + + +def Vec3(x, y, z): + return "new THREE.Vector3({}, {}, {})".format(x, y, z) + + +class GUITestBase(unittest.TestCase): + + WND = TREE = None + DLG = None + + def assertBox3(self, testName, box1, box2=UNDEF): + self.WND.runScript('assertBox3("{}", {}, {})'.format(testName, box1, box2)) + + def assertZRange(self, testName, obj="app.scene", min=UNDEF, max=UNDEF): + self.WND.runScript('assertZRange("{}", {}, {}, {})'.format(testName, obj, min, max)) + + def assertText(self, testName, text, startingElemId=None, partialMatch=False): + args = '"{}", "{}", {}'.format(testName, text, '"{}"'.format(startingElemId) if startingElemId else UNDEF) + + if partialMatch: + args += ", true" + + self.WND.runScript('assertText({})'.format(args)) + + def assertVisibility(self, testName, elemId, expected=True): + self.WND.runScript('assertVisibility("{}", "{}", {})'.format(testName, elemId, js_bool(expected))) + + def loadSettings(self, filename): + self.WND.loadSettings(filename) + self.sleep(1000) + self.WND.webPage.loadScriptFile(Script.TEST) + + def mouseClick(self, x, y): + self.WND.runScript("showMarker({}, {}, 400)".format(x, y)) + self.sleep(500) + QTest.mouseClick(self.WND.ui.webView, Qt.LeftButton, pos=QPoint(x, y)) + + @classmethod + def sleep(cls, msec=500): + loop = QEventLoop() + QTimer.singleShot(msec, loop.quit) + loop.exec_() + + @classmethod + def doEvents(cls): + cls.sleep(1) + + @classmethod + def waitBC(cls): + """wait for build to complete""" + cls.sleep(400) + + def tearDown(self): + self.sleep() + + +class SceneTest(GUITestBase): + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + cls.DLG.close() + + def test01_loadScene1(self): + self.loadSettings(dataPath("scene1_1.qto3settings")) + self.assertText("Test scene 1", "Test Scene 1", "header", partialMatch=True) + + def test02_ZRange(self): + # skip if map canvas extent and rotation are not expected status + mapSettings = self.WND.qgisIface.mapCanvas().mapSettings() + mapExtent = mapSettings.extent() + if not mapExtent.contains(QgsRectangle(-30000, -140000, 80000, -50000)) or mapSettings.rotation() != 0: + self.skipTest("Map canvas extent doesn't contain test area or map is rotated. map: " + mapExtent.toString()) + + self.assertZRange("scene z range", min=-4000, max=3776 + 600 * 5) # min: flat plane, max: pt4 6th feature + + def test10_openScenePDialog(self): + self.__class__.DLG = self.WND.showScenePropertiesDialog() + + +class LayerTestBase(GUITestBase): + + LAYER_ID = None + CAMERA_STATE = None + + @classmethod + def setUpClass(cls): + layer = cls.WND.iface.settings.getLayer(cls.LAYER_ID) + if layer is None: + cls.skipTest("Layer '{}' not found. Skipping test.".format(cls.LAYER_ID)) + + if cls.CAMERA_STATE: + cls.WND.webPage.setCameraState(cls.CAMERA_STATE) + + cls.DLG = cls.WND.showLayerPropertiesDialog(layer) + + @classmethod + def tearDownClass(cls): + cls.DLG.close() + + +class DEMLayerTest(LayerTestBase): + + LAYER_ID = "dem_srtm3020150914165149263" + + +class VLayerTestBase(LayerTestBase): + + def test01_objectTypes(self): + """PD: object type combo box test""" + combo = self.DLG.page.comboBox_ObjectType + for i in range(combo.count()): + combo.setCurrentIndex(i) + self.DLG.ui.buttonBox.button(QDialogButtonBox.Apply).click() + self.waitBC() + + +class PointLayerTest(VLayerTestBase): + + LAYER_ID = "pt120150915163204544" + CAMERA_STATE = { + 'lookAt': {'x': -7420, 'y': -116966, 'z': 0}, + 'pos': {'x': -34781, 'y': -143760, 'z': 34119} + } + PT4_LAYER_ID = "pt420150915163206372" + + def test02_clickObject(self): + self.mouseClick(525, 260) # second feature in pt3 layer (Z=500, h=1000*2) + self.assertText("clicked coords", " 2500.00", "qr_coords", partialMatch=True) + + def test03_clickObjectWithAttr(self): + self.mouseClick(485, 160) # third feature in pt4 layer + self.assertText("attribute", "cone 3", "qr_attrs_table") + + def test04_hideAndClick(self): + self.TREE.itemFromLayerId(self.PT4_LAYER_ID).setCheckState(Qt.Unchecked) # hide pt4 layer + self.waitBC() + + self.mouseClick(485, 160) # sea + self.assertText("hide layer", " 0.00", "qr_coords", partialMatch=True) + + def test05_restoreAndClick(self): + self.TREE.itemFromLayerId(self.PT4_LAYER_ID).setCheckState(Qt.Checked) # show pt4 layer + self.waitBC() + + self.mouseClick(485, 160) # third feature in pt4 layer + self.assertText("show layer", "cone 3", "qr_attrs_table") + + +class LineLayerTest(VLayerTestBase): + + LAYER_ID = "line120150915163207575" + CAMERA_STATE = { + 'lookAt': {'x': 40827, 'y': -106069, 'z': 0}, + 'pos': {'x': 9037, 'y': -142511, 'z': 24005} + } + + +class PolygonLayerTest(VLayerTestBase): + + LAYER_ID = "polygon120150915163203246" + CAMERA_STATE = { + 'lookAt': {'x': 62558, 'y': -94145, 'z': 0}, + 'pos': {'x': 90735, 'y': -127135, 'z': 16134} + } + + def test06_clickSpace(self): + self.mouseClick(600, 20) # sky + + self.assertVisibility("click space", "popup", False) + + +class WidgetTest(GUITestBase): + + def test01_naviZ(self): + self.mouseClick(735, 506) # +Z + self.sleep(1000) + + self.mouseClick(450, 150) # sea (dem_srtm30) + self.assertText("clicked coords", " 0.00", "qr_coords", partialMatch=True) + + def test02_naviX(self): + self.mouseClick(766, 535) # +X + self.sleep(1000) + + self.mouseClick(400, 400) # flat plane + self.assertText("clicked coords", " -4000.00", "qr_coords", partialMatch=True) + + +# Test result +class JSException(Exception): + + pass + + +class JSTestException(Exception): + + pass + + +class GUITestResult(unittest.TestResult): + + DUMMY_TEST = unittest.TestCase() + VERBOSE = True + + def __init__(self, stream=None, descriptions=None, verbosity=None): + super().__init__(stream, descriptions, verbosity) + + self.consoleMessages = {} + + def addConsoleMessage(self, message, lineNumber, sourceID): + if ".py" in sourceID: + return + + source = "{} ({})".format(sourceID.split("/")[-1], lineNumber) + + if "error" in message.lower(): + e = JSException(source, message) + self.addError(self.DUMMY_TEST, (type(e), e, e.__traceback__)) + + key = "{}: {}".format(source, message) + self.consoleMessages[key] = self.consoleMessages.get(key, 0) + 1 + + def addTestResult(self, testName, result, msg): + if not result: + e = JSTestException(testName, msg) + self.addFailure(self.DUMMY_TEST, (type(e), e, e.__traceback__)) + + logMessage("'{}' ({}) {}".format(testName, "success" if result else "err/fail", msg), warning=not result) + + def printResult(self): + rows = ["", "### Results ###"] + rows.append("{} tests, {} skipped, {} errors, {} failures".format(self.testsRun, len(self.skipped), len(self.errors), len(self.failures))) + + to_remove = "Qgis2threejs.tests.gui.test_gui." + + if self.skipped: + rows.append("# Skipped") + for test, text in self.errors: + rows.append("* " + text.replace(to_remove, "")) + + if self.errors: + rows.append("# Errors") + for test, text in self.errors: + rows.append("* " + text.replace(to_remove, "")) + + if self.failures: + rows.append("# Failures") + for test, text in self.failures: + rows.append("* " + text.replace(to_remove, "")) + + rows.append("### Console Messages ###") + for msg, count in self.consoleMessages.items(): + rows.append("* {} [x{}]".format(msg, count)) + + rows.append("See web inspector for details.") + + logMessage("\n".join(rows), warning=bool(self.errors or self.failures)) + + def startTest(self, test): + super().startTest(test) + + desc = test.shortDescription() or "" + + if self.VERBOSE: + logMessage("'{}' {}".format(".".join(test.id().split(".")[-2:]), desc), warning=False) + + +def runTest(wnd): + + project = QgsProject.instance() + filename = os.path.basename(project.fileName()) + + if filename != "testproject1.qgs": + QMessageBox.warning(wnd, "Test", "Load 'testproject1.qgs' and retry.") + return + + initOutputDir() + + geomFile = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wnd_geom.bin") + if os.path.exists(geomFile): + with open(geomFile, "rb") as f: + wnd.restoreGeometry(f.read()) + + logMessage("Window geometry restored from file.") + + else: + with open(geomFile, "wb") as f: + f.write(wnd.saveGeometry()) + + logMessage("Window geometry saved to a file.") + + testClasses = [SceneTest, PointLayerTest, LineLayerTest, PolygonLayerTest, WidgetTest] + suite = unittest.TestSuite() + + for testClass in testClasses: + testClass.WND = wnd + testClass.TREE = wnd.ui.treeView + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(testClass)) + + result = GUITestResult() + wnd.webPage.bridge.testResultReceived.connect(result.addTestResult) + + # a monkey patch to wnd + wnd._printConsoleMessage = wnd.printConsoleMessage + + def printConsoleMessage(self, message, lineNumber="", sourceID=""): + + wnd._printConsoleMessage(message, lineNumber, sourceID) + + result.addConsoleMessage(message, lineNumber, sourceID) + + wnd.printConsoleMessage = printConsoleMessage.__get__(wnd) + + # start testing + logMessage("Testing GUI...", warning=False) + + try: + suite(result) + + finally: + pass + + result.printResult() + + wnd.printConsoleMessage = wnd._printConsoleMessage diff --git a/tests/gui/wnd_geom.bin b/tests/gui/wnd_geom.bin new file mode 100644 index 0000000000000000000000000000000000000000..45e15e7cc85d9c996f743d79bb71cdfedf6ae569 GIT binary patch literal 66 ucmZR)dEqnzGXn^O0