From f983ca34af97bdc3c43f488cea601b6093f393b5 Mon Sep 17 00:00:00 2001 From: Kaustuv Pokharel Date: Wed, 17 Jun 2026 14:33:43 -0400 Subject: [PATCH 1/6] tests for the reshape geometry editor tool --- test/qml/tst_geometryeditor_reshape.qml | 168 ++++++++++++++++++++++++ test/test_qml.cpp | 3 + 2 files changed, 171 insertions(+) create mode 100644 test/qml/tst_geometryeditor_reshape.qml diff --git a/test/qml/tst_geometryeditor_reshape.qml b/test/qml/tst_geometryeditor_reshape.qml new file mode 100644 index 0000000000..6da40d8093 --- /dev/null +++ b/test/qml/tst_geometryeditor_reshape.qml @@ -0,0 +1,168 @@ +import QtQuick +import QtTest +import org.qfield +import org.qgis +import Theme +import "../../src/qml/geometryeditors" as GeometryEditors + +// The reshape maths is covered in test_geometryutils.cpp, so here we just check the tool wiring around it: init, +// the blocking state, the failure path (toast and rollback), and cancel. A valid +// reshape through confirm() needs live digitizing state to be reproduced, so +// that path is left to the C++ tests. + +TestCase { + id: testCase + name: "GeometryEditorReshape" + + property var fieldsLayer: qgisProject.mapLayersByName("Fields")[0] + property string lastToastType: "" + + function init() { + lastToastType = ""; + } + + function cleanup() { + reshapeTool.cancel(); + if (fieldsLayer.editBuffer()) { + fieldsLayer.rollBack(); + } + } + + // reshape works on the current features layer, set up the feature model and + // hand the tool a fresh rubberband like the app does through init + function initReshapeOnFields() { + featureModel.currentLayer = fieldsLayer; + featureModel.feature = fieldsLayer.getFeature("39"); + rubberband.vectorLayer = fieldsLayer; + reshapeTool.init(featureModel, mapSettingsItem, rubberband, null); + return featureModel; + } + + MapSettings { + id: mapSettingsItem + destinationCrs: CoordinateReferenceSystemUtils.fromDescription("EPSG:3857") + } + + RubberbandModel { + id: rubberband + crs: CoordinateReferenceSystemUtils.fromDescription("EPSG:3857") + } + + FeatureModel { + id: featureModel + project: qgisProject + + vertexModel: VertexModel { + id: geometryEditingVertexModel + } + } + + GeometryEditors.Reshape { + id: reshapeTool + featureModel: featureModel + mapSettings: mapSettingsItem + } + + function test_initSetsUpToolForPolygonReshape() { + initReshapeOnFields(); + + // init wires the feature model and forces the rubberband to polygon mode, + // since reshape only operates on polygon rings + compare(reshapeTool.featureModel, featureModel); + compare(Number(rubberband.geometryType), Qgis.GeometryType.Polygon); + } + + function test_blockingFollowsRubberbandVertexCount() { + initReshapeOnFields(); + // blocking mirrors isDigitizing, which is vertexCount > 1 + compare(reshapeTool.blocking, false); + + rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); + rubberband.addVertexFromPoint(GeometryUtils.point(1031000, 5911500)); + + compare(reshapeTool.blocking, true); + } + + function test_confirmWithInvalidLineToastsAndDoesNotChangeGeometry() { + const model = initReshapeOnFields(); + const before = fieldsLayer.getFeature("39").geometry.asWkt(); + + // a single point is not a valid reshape line, so reshapeFromRubberband + // returns non-Success. the confirm handler should toast an error and roll + // back, leaving the feature untouched + rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); + reshapeTool.children[0].confirm(); + + compare(lastToastType, "error"); + const after = fieldsLayer.getFeature("39").geometry.asWkt(); + compare(after, before); + } + + function test_cancelResetsRubberband() { + initReshapeOnFields(); + rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); + rubberband.addVertexFromPoint(GeometryUtils.point(1031000, 5911500)); + verify(rubberband.vertexCount > 1); + + reshapeTool.cancel(); + + // cancel clears the rubberband back down + verify(rubberband.vertexCount <= 1); + } + + // scope objects the tool and DigitizingToolbar expect from the app + Item { + id: mainWindow + property var contentItem: mainWindow + } + + Item { + id: dashBoard + property bool shouldReturnHome: false + } + + Item { + id: stateMachine + property string state: "digitize" + } + + function displayToast(message, type, actionText, actionCallback) { + lastToastType = type !== undefined ? type : ""; + } + + Item { + id: qfieldSettings + property bool autoSave: false + } + + Item { + id: coordinateLocator + property var currentCoordinate: GeometryUtils.point(0, 0) + property string positionInformation: "" + property string topSnappingResult: "" + property bool positionLocked: false + function flash() {} + } + + Item { + id: projectInfo + property string cloudUserInformation: "" + } + + Item { + id: positionSource + property string positionInformation: "" + property bool averagedPosition: false + property int averagedPositionCount: 0 + } + + Item { + id: positioningSettings + property bool averagedPositioning: false + property int averagedPositioningMinimumCount: 0 + property bool averagedPositioningAutomaticStop: false + property bool accuracyIndicator: false + property bool accuracyRequirement: false + property real accuracyBad: 0 + } +} diff --git a/test/test_qml.cpp b/test/test_qml.cpp index dd4d385370..0956bd2d1f 100644 --- a/test/test_qml.cpp +++ b/test/test_qml.cpp @@ -16,6 +16,7 @@ ***************************************************************************/ #include "appinterface.h" +#include "cogo/cogoregistry.h" #include "platformutilities.h" #include "qfield.h" #include "qfield_qml_init.h" @@ -195,6 +196,8 @@ class Setup : public QObject iface->setParent( engine ); AppInterface::setInstance( iface ); engine->rootContext()->setContextProperty( QStringLiteral( "iface" ), iface ); + CogoRegistry *cogoRegistry = new CogoRegistry( engine ); + CogoRegistry::setInstance( cogoRegistry ); } }; From 4e2c2a4b3e4bcdcead7ca29576980d66326f27ae Mon Sep 17 00:00:00 2001 From: Kaustuv Pokharel Date: Wed, 17 Jun 2026 14:42:05 -0400 Subject: [PATCH 2/6] guard reshape test against missing Fields/39 fixture feature --- test/qml/tst_geometryeditor_reshape.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/qml/tst_geometryeditor_reshape.qml b/test/qml/tst_geometryeditor_reshape.qml index 6da40d8093..9756eba85a 100644 --- a/test/qml/tst_geometryeditor_reshape.qml +++ b/test/qml/tst_geometryeditor_reshape.qml @@ -19,6 +19,7 @@ TestCase { function init() { lastToastType = ""; + verify(fieldsLayer.getFeature("39").isValid()); } function cleanup() { @@ -141,7 +142,8 @@ TestCase { property string positionInformation: "" property string topSnappingResult: "" property bool positionLocked: false - function flash() {} + function flash() { + } } Item { From 9b910ac59ccdf06e844bceb8d83bde58b208c39c Mon Sep 17 00:00:00 2001 From: Kaustuv Pokharel Date: Wed, 17 Jun 2026 15:33:00 -0400 Subject: [PATCH 3/6] revert- removed guard --- test/qml/tst_geometryeditor_reshape.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/test/qml/tst_geometryeditor_reshape.qml b/test/qml/tst_geometryeditor_reshape.qml index 9756eba85a..6fbd491cb5 100644 --- a/test/qml/tst_geometryeditor_reshape.qml +++ b/test/qml/tst_geometryeditor_reshape.qml @@ -19,7 +19,6 @@ TestCase { function init() { lastToastType = ""; - verify(fieldsLayer.getFeature("39").isValid()); } function cleanup() { From e3b0928465c951c38600eb821a1018542b88d84f Mon Sep 17 00:00:00 2001 From: Kaustuv Pokharel Date: Sun, 21 Jun 2026 17:42:11 -0400 Subject: [PATCH 4/6] test reshape with a valid line and assert exact rubberband counts --- test/qml/tst_geometryeditor_reshape.qml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test/qml/tst_geometryeditor_reshape.qml b/test/qml/tst_geometryeditor_reshape.qml index 6fbd491cb5..c99a8b8448 100644 --- a/test/qml/tst_geometryeditor_reshape.qml +++ b/test/qml/tst_geometryeditor_reshape.qml @@ -83,6 +83,24 @@ TestCase { compare(reshapeTool.blocking, true); } + function test_reshapeWithValidLineProducesExpectedGeometry() { + const model = initReshapeOnFields(); + // a reshape line that crosses the polygon boundary twice, cutting a new edge + rubberband.addVertexFromPoint(GeometryUtils.point(1030845.75, 5911397.39)); + rubberband.addVertexFromPoint(GeometryUtils.point(1030771.49, 5911511.09)); + rubberband.addVertexFromPoint(GeometryUtils.point(1030857.23, 5911624.79)); + + // drive the operation directly and roll back instead of committing, so the + // shared layer data is untouched + if (!fieldsLayer.editBuffer()) + fieldsLayer.startEditing(); + const result = GeometryUtils.reshapeFromRubberband(fieldsLayer, model.feature.id, rubberband); + // the reshape succeeds and produces this exact polygon + compare(Number(result), Number(GeometryUtils.Success)); + const expected = "Polygon ((1031040.99 5911336.9, 1030978.97 5911394.33, 1030845.75 5911397.39, 1030845.75 5911397.4, 1030857.23 5911624.79, 1031057.07 5911646.23, 1031082.33 5911535.21, 1031119.08 5911493.1, 1031093.82 5911453.28, 1031072.67 5911421.57, 1031063.19 5911407.34, 1031044.82 5911362.94, 1031041.44 5911340.01, 1031040.99 5911336.9))"; + compare(fieldsLayer.getFeature(model.feature.id).geometry.asWkt(2), expected); + } + function test_confirmWithInvalidLineToastsAndDoesNotChangeGeometry() { const model = initReshapeOnFields(); const before = fieldsLayer.getFeature("39").geometry.asWkt(); @@ -102,12 +120,12 @@ TestCase { initReshapeOnFields(); rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); rubberband.addVertexFromPoint(GeometryUtils.point(1031000, 5911500)); - verify(rubberband.vertexCount > 1); + compare(rubberband.vertexCount, 3); reshapeTool.cancel(); - // cancel clears the rubberband back down - verify(rubberband.vertexCount <= 1); + // cancel resets the rubberband to empty + compare(rubberband.vertexCount, 1); } // scope objects the tool and DigitizingToolbar expect from the app From 0bab73558e7353f1aeb90aa550efbfdaa6628e80 Mon Sep 17 00:00:00 2001 From: Kaustuv Pokharel Date: Sat, 27 Jun 2026 01:21:20 -0400 Subject: [PATCH 5/6] test reshape through tool confirm and restore feature in cleanup --- test/qml/tst_geometryeditor_reshape.qml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/qml/tst_geometryeditor_reshape.qml b/test/qml/tst_geometryeditor_reshape.qml index c99a8b8448..e474a75138 100644 --- a/test/qml/tst_geometryeditor_reshape.qml +++ b/test/qml/tst_geometryeditor_reshape.qml @@ -5,17 +5,13 @@ import org.qgis import Theme import "../../src/qml/geometryeditors" as GeometryEditors -// The reshape maths is covered in test_geometryutils.cpp, so here we just check the tool wiring around it: init, -// the blocking state, the failure path (toast and rollback), and cancel. A valid -// reshape through confirm() needs live digitizing state to be reproduced, so -// that path is left to the C++ tests. - TestCase { id: testCase name: "GeometryEditorReshape" property var fieldsLayer: qgisProject.mapLayersByName("Fields")[0] property string lastToastType: "" + property var originalGeometry: null function init() { lastToastType = ""; @@ -23,6 +19,13 @@ TestCase { function cleanup() { reshapeTool.cancel(); + if (originalGeometry !== null) { + featureModel.currentLayer = fieldsLayer; + featureModel.feature = fieldsLayer.getFeature("39"); + featureModel.changeGeometry(originalGeometry); + featureModel.save(); + originalGeometry = null; + } if (fieldsLayer.editBuffer()) { fieldsLayer.rollBack(); } @@ -85,18 +88,18 @@ TestCase { function test_reshapeWithValidLineProducesExpectedGeometry() { const model = initReshapeOnFields(); + // store the original so cleanup can restore it after confirm commits + originalGeometry = fieldsLayer.getFeature("39").geometry; + // a reshape line that crosses the polygon boundary twice, cutting a new edge rubberband.addVertexFromPoint(GeometryUtils.point(1030845.75, 5911397.39)); rubberband.addVertexFromPoint(GeometryUtils.point(1030771.49, 5911511.09)); rubberband.addVertexFromPoint(GeometryUtils.point(1030857.23, 5911624.79)); - // drive the operation directly and roll back instead of committing, so the - // shared layer data is untouched - if (!fieldsLayer.editBuffer()) - fieldsLayer.startEditing(); - const result = GeometryUtils.reshapeFromRubberband(fieldsLayer, model.feature.id, rubberband); + // drive the tool through confirm so its onConfirmed runs the reshape + reshapeTool.children[0].confirm(); + // the reshape succeeds and produces this exact polygon - compare(Number(result), Number(GeometryUtils.Success)); const expected = "Polygon ((1031040.99 5911336.9, 1030978.97 5911394.33, 1030845.75 5911397.39, 1030845.75 5911397.4, 1030857.23 5911624.79, 1031057.07 5911646.23, 1031082.33 5911535.21, 1031119.08 5911493.1, 1031093.82 5911453.28, 1031072.67 5911421.57, 1031063.19 5911407.34, 1031044.82 5911362.94, 1031041.44 5911340.01, 1031040.99 5911336.9))"; compare(fieldsLayer.getFeature(model.feature.id).geometry.asWkt(2), expected); } From 43e607bc14db9cc8a81c66a413ec24127e3606a2 Mon Sep 17 00:00:00 2001 From: Kaustuv Pokharel Date: Sat, 27 Jun 2026 03:33:40 -0400 Subject: [PATCH 6/6] test reshape on memory layer through toolbar --- src/qml/geometryeditors/Reshape.qml | 1 + test/qml/tst_geometryeditor_reshape.qml | 88 ++++++++++++------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/qml/geometryeditors/Reshape.qml b/src/qml/geometryeditors/Reshape.qml index bc4904b247..a7059ca6e2 100644 --- a/src/qml/geometryeditors/Reshape.qml +++ b/src/qml/geometryeditors/Reshape.qml @@ -32,6 +32,7 @@ GeometryEditorBase { DigitizingToolbar { id: drawPolygonToolbar + objectName: "reshapeDigitizingToolbar" showConfirmButton: true screenHovering: reshapeToolbar.screenHovering diff --git a/test/qml/tst_geometryeditor_reshape.qml b/test/qml/tst_geometryeditor_reshape.qml index e474a75138..d8516552c7 100644 --- a/test/qml/tst_geometryeditor_reshape.qml +++ b/test/qml/tst_geometryeditor_reshape.qml @@ -3,44 +3,46 @@ import QtTest import org.qfield import org.qgis import Theme +import "Utils.js" as Utils import "../../src/qml/geometryeditors" as GeometryEditors TestCase { id: testCase name: "GeometryEditorReshape" - property var fieldsLayer: qgisProject.mapLayersByName("Fields")[0] + property var testLayer: null property string lastToastType: "" - property var originalGeometry: null + readonly property string squareJson: '{"type":"FeatureCollection","features":[{"type":"Feature","id":0,"geometry":{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]},"properties":{}}]}' function init() { lastToastType = ""; + testLayer = LayerUtils.memoryLayerFromJsonString("reshape_test", squareJson, CoordinateReferenceSystemUtils.fromDescription("EPSG:3857")); } function cleanup() { reshapeTool.cancel(); - if (originalGeometry !== null) { - featureModel.currentLayer = fieldsLayer; - featureModel.feature = fieldsLayer.getFeature("39"); - featureModel.changeGeometry(originalGeometry); - featureModel.save(); - originalGeometry = null; - } - if (fieldsLayer.editBuffer()) { - fieldsLayer.rollBack(); - } + testLayer = null; } - // reshape works on the current features layer, set up the feature model and + // set up the feature model on the memory layer and // hand the tool a fresh rubberband like the app does through init - function initReshapeOnFields() { - featureModel.currentLayer = fieldsLayer; - featureModel.feature = fieldsLayer.getFeature("39"); - rubberband.vectorLayer = fieldsLayer; + function initReshapeOnSquare() { + featureModel.currentLayer = testLayer; + featureModel.feature = testLayer.getFeature(1); + rubberband.vectorLayer = testLayer; reshapeTool.init(featureModel, mapSettingsItem, rubberband, null); return featureModel; } + function addToolVertex(toolbar, x, y) { + rubberband.currentCoordinate = GeometryUtils.point(x, y); + toolbar.addVertex(); + } + + function toolbar() { + return Utils.findChildren(reshapeTool, "reshapeDigitizingToolbar"); + } + MapSettings { id: mapSettingsItem destinationCrs: CoordinateReferenceSystemUtils.fromDescription("EPSG:3857") @@ -67,7 +69,7 @@ TestCase { } function test_initSetsUpToolForPolygonReshape() { - initReshapeOnFields(); + initReshapeOnSquare(); // init wires the feature model and forces the rubberband to polygon mode, // since reshape only operates on polygon rings @@ -76,58 +78,56 @@ TestCase { } function test_blockingFollowsRubberbandVertexCount() { - initReshapeOnFields(); + initReshapeOnSquare(); // blocking mirrors isDigitizing, which is vertexCount > 1 compare(reshapeTool.blocking, false); - rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); - rubberband.addVertexFromPoint(GeometryUtils.point(1031000, 5911500)); + const tb = toolbar(); + addToolVertex(tb, 5, 5); + addToolVertex(tb, 6, 6); compare(reshapeTool.blocking, true); } function test_reshapeWithValidLineProducesExpectedGeometry() { - const model = initReshapeOnFields(); - // store the original so cleanup can restore it after confirm commits - originalGeometry = fieldsLayer.getFeature("39").geometry; + initReshapeOnSquare(); + const tb = toolbar(); - // a reshape line that crosses the polygon boundary twice, cutting a new edge - rubberband.addVertexFromPoint(GeometryUtils.point(1030845.75, 5911397.39)); - rubberband.addVertexFromPoint(GeometryUtils.point(1030771.49, 5911511.09)); - rubberband.addVertexFromPoint(GeometryUtils.point(1030857.23, 5911624.79)); + // a reshape line that cuts across the top right corner of the square + addToolVertex(tb, 10, 5); + addToolVertex(tb, 5, 10); // drive the tool through confirm so its onConfirmed runs the reshape - reshapeTool.children[0].confirm(); + tb.confirm(); - // the reshape succeeds and produces this exact polygon - const expected = "Polygon ((1031040.99 5911336.9, 1030978.97 5911394.33, 1030845.75 5911397.39, 1030845.75 5911397.4, 1030857.23 5911624.79, 1031057.07 5911646.23, 1031082.33 5911535.21, 1031119.08 5911493.1, 1031093.82 5911453.28, 1031072.67 5911421.57, 1031063.19 5911407.34, 1031044.82 5911362.94, 1031041.44 5911340.01, 1031040.99 5911336.9))"; - compare(fieldsLayer.getFeature(model.feature.id).geometry.asWkt(2), expected); + // the corner is cut off, replaced by the reshape line + const expected = "Polygon ((5 10, 0 10, 0 0, 10 0, 10 5, 5 10))"; + compare(testLayer.getFeature(1).geometry.asWkt(2), expected); } function test_confirmWithInvalidLineToastsAndDoesNotChangeGeometry() { - const model = initReshapeOnFields(); - const before = fieldsLayer.getFeature("39").geometry.asWkt(); + initReshapeOnSquare(); + const before = testLayer.getFeature(1).geometry.asWkt(); + const tb = toolbar(); // a single point is not a valid reshape line, so reshapeFromRubberband // returns non-Success. the confirm handler should toast an error and roll // back, leaving the feature untouched - rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); - reshapeTool.children[0].confirm(); + addToolVertex(tb, 5, 5); + tb.confirm(); compare(lastToastType, "error"); - const after = fieldsLayer.getFeature("39").geometry.asWkt(); + const after = testLayer.getFeature(1).geometry.asWkt(); compare(after, before); } function test_cancelResetsRubberband() { - initReshapeOnFields(); - rubberband.addVertexFromPoint(GeometryUtils.point(1030900, 5911400)); - rubberband.addVertexFromPoint(GeometryUtils.point(1031000, 5911500)); + initReshapeOnSquare(); + const tb = toolbar(); + addToolVertex(tb, 5, 5); + addToolVertex(tb, 6, 6); compare(rubberband.vertexCount, 3); - - reshapeTool.cancel(); - - // cancel resets the rubberband to empty + tb.cancel(); compare(rubberband.vertexCount, 1); }