From 338723d50f6095644b0c4593508edb2ea9a07694 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Wed, 10 Jun 2026 09:55:45 +0300 Subject: [PATCH 1/3] Added functions in test utils to create value relation lookup and ordering layer Added test cases for form editors suite for conversions and lookup Added test cases for test models suite for ordering, search and hot reload --- app/test/testformeditors.cpp | 317 +++++++++++++---------- app/test/testformeditors.h | 3 +- app/test/testmodels.cpp | 480 ++++++++++++++++------------------- app/test/testmodels.h | 6 +- app/test/testutils.cpp | 56 ++++ app/test/testutils.h | 13 + 6 files changed, 471 insertions(+), 404 deletions(-) diff --git a/app/test/testformeditors.cpp b/app/test/testformeditors.cpp index c1b52f420..b7fda654c 100644 --- a/app/test/testformeditors.cpp +++ b/app/test/testformeditors.cpp @@ -449,151 +449,204 @@ void TestFormEditors::testRelationsWidgetPresence() QVERIFY( relationReferencesCount == 1 ); } -void TestFormEditors::testValueRelationsEditor() {} -// { -// /* Test project: project_value_relations -// * It has value relations sets up followingly: -// * -// * - Main Layer has VR to: -// * - sub layer -// * - subsub layer ( with filter expression that subsub is categorized based on sub ) -// * - another layer ( key is not fid, but textual ) -// */ - -// QString projectDir = TestUtils::testDataDir() + "/project_value_relations"; -// QString projectName = "proj.qgz"; - -// QVERIFY( QgsProject::instance()->read( projectDir + "/" + projectName ) ); - -// QgsMapLayer *mainL = QgsProject::instance()->mapLayersByName( QStringLiteral( "main" ) ).at( 0 ); -// QgsVectorLayer *mainLayer = static_cast( mainL ); - -// QVERIFY( mainLayer && mainLayer->isValid() ); - -// QgsMapLayer *subL = QgsProject::instance()->mapLayersByName( QStringLiteral( "sub" ) ).at( 0 ); -// QgsVectorLayer *subLayer = static_cast( subL ); - -// QVERIFY( subLayer && subLayer->isValid() ); - -// QgsMapLayer *subsubL = QgsProject::instance()->mapLayersByName( QStringLiteral( "subsub" ) ).at( 0 ); -// QgsVectorLayer *subsubLayer = static_cast( subsubL ); - -// QVERIFY( subsubLayer && subsubLayer->isValid() ); - -// QgsMapLayer *anotherL = QgsProject::instance()->mapLayersByName( QStringLiteral( "another" ) ).at( 0 ); -// QgsVectorLayer *anotherLayer = static_cast( anotherL ); - -// QVERIFY( anotherLayer && anotherLayer->isValid() ); - -// // test ValueRelationsFeaturesModel (drawer model) and ValueRelationController - -// QgsFeature f = mainLayer->getFeature( 1 ); -// FeatureLayerPair pair( f, mainLayer ); - -// AttributeController controller; -// controller.setFeatureLayerPair( pair ); - -// const TabItem *tab = controller.tabItem( 0 ); -// QVector items = tab->formItems(); - -// QVERIFY( items.length() == 5 ); - -// // order: 0 - fid, 1 - Name, 2 - subfk, 3 - anotherfk, 4 - subsubfk - -// // ------- FIELD SubFK: drawer model loads all sub-layer features on demand - -// const FormItem *subFkItem = controller.formItem( items.at( 2 ) ); - -// ValueRelationFeaturesModel subVRModel; -// QSignalSpy subSpy( &subVRModel, &LayerFeaturesModel::fetchingResultsChanged ); - -// subVRModel.setConfig( subFkItem->editorWidgetConfig() ); - -// // No features before explicit load (lazy loading) -// QCOMPARE( subVRModel.rowCount(), 0 ); - -// subVRModel.reloadFeatures(); -// subSpy.wait(); -// QCOMPARE( subVRModel.rowCount(), subLayer->dataProvider()->featureCount() ); -// QCOMPARE( subVRModel.layer()->id(), subLayer->id() ); - -// // KeyColumn and ValueColumn roles are present -// QVERIFY( subVRModel.data( subVRModel.index( 0, 0 ), ValueRelationFeaturesModel::KeyColumn ).isValid() ); -// QVERIFY( subVRModel.data( subVRModel.index( 0, 0 ), ValueRelationFeaturesModel::ValueColumn ).isValid() ); +void TestFormEditors::testValueRelationConversions() +{ + /* Tests qgisFormatToArray() and arrayToQgisFormat() for null, single, + * and multi values. + * + * AllowMulti=false: + * QGIS value → keys: "1" → ["1"] + * keys → QGIS value: ["42"] → "42" + * + * AllowMulti=true: + * QGIS value → keys: "{1,3,4}" → ["1","3","4"] + * keys → QGIS value: ["1","3","4"] → "{1,3,4}"; ["1"] → "{1}" + */ + QgsProject::instance()->removeAllMapLayers(); -// // ------- FIELD SubSubFK: form-scoped FilterExpression + drill-down + QgsVectorLayer *layer = TestUtils::createVRLookupLayer( 5 ); + QVERIFY( layer && layer->isValid() ); + QgsProject::instance()->addMapLayer( layer ); -// const FormItem *subsubFkItem = controller.formItem( items.at( 4 ) ); + // Single value test + ValueRelationController controller; + controller.setConfig( + { + { QStringLiteral( "Layer" ), layer->id() }, + { QStringLiteral( "Key" ), QStringLiteral( "key" ) }, + { QStringLiteral( "Value" ), QStringLiteral( "label" ) }, + { QStringLiteral( "AllowMulti" ), false } + } ); -// ValueRelationFeaturesModel subsubVRModel; -// QSignalSpy subsubSpy( &subsubVRModel, &LayerFeaturesModel::fetchingResultsChanged ); -// subsubVRModel.setConfig( subsubFkItem->editorWidgetConfig() ); -// subsubVRModel.setPair( pair ); // form scope resolves current_value() in the filter -// subsubVRModel.reloadFeatures(); + // null/empty : empty list + QCOMPARE( controller.qgisFormatToArray( QVariant() ), QStringList() ); + QCOMPARE( controller.qgisFormatToArray( QStringLiteral( "" ) ), QStringList() ); -// subsubSpy.wait(); -// QCOMPARE( subsubVRModel.layer()->id(), subsubLayer->id() ); + // single value "1" : ["1"] + QCOMPARE( controller.qgisFormatToArray( QStringLiteral( "1" ) ), + QStringList( { QStringLiteral( "1" ) } ) ); -// // With the pair set the form-scoped filter must restrict the result set -// QCOMPARE( subsubVRModel.rowCount(), 2 ); + // empty keys : "" + QCOMPARE( controller.arrayToQgisFormat( {} ), QString() ); -// // Filter expression is present and valid in the request -// QgsFeatureRequest request; -// subsubVRModel.setupFeatureRequest( request ); -// QVERIFY( !request.filterExpression()->operator QString().isEmpty() ); -// QVERIFY( request.filterExpression()->isValid() ); + // single key : plain value + QCOMPARE( controller.arrayToQgisFormat( { QStringLiteral( "42" ) } ), + QStringLiteral( "42" ) ); -// // Search combined with the filter expression -// subsubVRModel.setSearchExpression( QStringLiteral( "2" ) ); -// subsubSpy.wait(); -// QCOMPARE( subsubVRModel.rowCount(), 1 ); + // Multi value test + controller.setConfig( + { + { QStringLiteral( "Layer" ), layer->id() }, + { QStringLiteral( "Key" ), QStringLiteral( "key" ) }, + { QStringLiteral( "Value" ), QStringLiteral( "label" ) }, + { QStringLiteral( "AllowMulti" ), true } + } ); -// // featureTitle returns the value column -// { -// QModelIndex idx = subsubVRModel.index( 0, 0 ); -// FeatureLayerPair tempPair = subsubVRModel.data( idx, FeaturesModel::FeaturePair ).value(); -// QCOMPARE( subsubVRModel.featureTitle( tempPair ), QStringLiteral( "A2" ) ); -// } + // null : empty list + QCOMPARE( controller.qgisFormatToArray( QVariant() ), QStringList() ); -// // ------- FIELD AnotherFK: helper-based conversions and invalidation + // "{1,3,4}" : ["1","3","4"] + QCOMPARE( controller.qgisFormatToArray( QStringLiteral( "{1,3,4}" ) ), + QStringList( { QStringLiteral( "1" ), QStringLiteral( "3" ), QStringLiteral( "4" ) } ) ); -// const FormItem *anotherFkItem = controller.formItem( items.at( 3 ) ); + // "{1}" : ["1"] + QCOMPARE( controller.qgisFormatToArray( QStringLiteral( "{1}" ) ), + QStringList( { QStringLiteral( "1" ) } ) ); -// ValueRelationFeaturesModel anotherVRModel; -// QSignalSpy anotherSpy( &anotherVRModel, &LayerFeaturesModel::fetchingResultsChanged ); -// anotherVRModel.setConfig( anotherFkItem->editorWidgetConfig() ); -// anotherVRModel.reloadFeatures(); -// anotherSpy.wait(); -// QCOMPARE( anotherVRModel.rowCount(), anotherLayer->dataProvider()->featureCount() ); -// QCOMPARE( anotherVRModel.layer()->id(), anotherLayer->id() ); + // empty keys : "" + QCOMPARE( controller.arrayToQgisFormat( {} ), QString() ); -// // ValueRelationController handles conversions (static) and lookups (instance). -// // The "another" layer uses text keys; we look up a single known key "B". -// ValueRelationController anotherHelper; -// anotherHelper.setConfig( anotherFkItem->editorWidgetConfig() ); + // ["1","3","4"] : "{1,3,4}" + QCOMPARE( controller.arrayToQgisFormat( { QStringLiteral( "1" ), QStringLiteral( "3" ), QStringLiteral( "4" ) } ), + QStringLiteral( "{1,3,4}" ) ); -// // Single key lookup: pick the first key from the already-loaded model so the -// // test is not sensitive to the exact fixture values. -// QVERIFY( anotherVRModel.rowCount() > 0 ); -// const QVariant firstKey = anotherVRModel.data( anotherVRModel.index( 0, 0 ), ValueRelationFeaturesModel::KeyColumn ); -// QVERIFY( firstKey.isValid() ); + // ["1"] : "{1}" + QCOMPARE( controller.arrayToQgisFormat( { QStringLiteral( "1" ) } ), + QStringLiteral( "{1}" ) ); -// QSignalSpy lookupSpy( &anotherHelper, &ValueRelationController::displayValuesReady ); -// anotherHelper.lookupDisplayValues( firstKey ); -// QVERIFY( lookupSpy.wait() ); -// QCOMPARE( lookupSpy.last().at( 0 ).toList().size(), 1 ); + QgsProject::instance()->removeAllMapLayers(); +} -// // Static: round-trip QGIS format (type-independent, no layer access) -// QCOMPARE( anotherHelper.convertToQgisFormat( { QStringLiteral( "B" ), QStringLiteral( "C" ) } ), -// QStringLiteral( "{B,C}" ) ); -// QCOMPARE( anotherHelper.convertFromQgisFormat( QStringLiteral( "{B,C}" ), true ), -// QStringList( { QStringLiteral( "B" ), QStringLiteral( "C" ) } ) ); +void TestFormEditors::testValueRelationControllerLookup() +{ + /* Tests async display-text lookup for ValueRelationController: + * + * baseConfig controller (no FilterExpression): + * 1. Missing key → presentRawValue fires; invalidateSelection does NOT fire; displayText="" + * 2. lookupDisplayTextOnHotreload without FilterExpression → returns early, no signals + * 3. Basic lookup: lookupDisplayTextOnValueChanged("1") → "Cat1-A" + * + * filterConfig controller (FilterExpression set): + * 4. lookupDisplayTextOnHotreload, key valid in context → displayText updated ("Cat1-A") + * 5. lookupDisplayTextOnHotreload, key no longer in context → invalidateSelection fires + * + * Cases 1-2 run before case 3 so that mDisplayText is "" when case 1 checks + * displaySpy.isEmpty() — the setDisplayText guard skips emission for equal values. + * + * Lookup layer: + * key=1 label="Cat1-A" category=1 + * key=2 label="Cat1-B" category=1 + * key=3 label="Cat2-A" category=2 + * key=4 label="Cat2-B" category=2 + * + * Filter expression: "category" = current_value('cat') + * Form feature cat=1 → keys 1,2 reachable; cat=2 → keys 3,4 reachable. + */ + QgsProject::instance()->removeAllMapLayers(); + + // create the lookup layer + QgsVectorLayer *lookupLayer = new QgsVectorLayer( + QStringLiteral( "None?field=key:integer&field=label:string&field=category:integer" ), + QStringLiteral( "vr_lookup" ), + QStringLiteral( "memory" ) + ); + QVERIFY( lookupLayer && lookupLayer->isValid() ); + + const QStringList labels = { QStringLiteral( "Cat1-A" ), QStringLiteral( "Cat1-B" ), + QStringLiteral( "Cat2-A" ), QStringLiteral( "Cat2-B" ) + }; + QgsFeatureList features; + for ( int i = 0; i < 4; ++i ) + { + QgsFeature feature( lookupLayer->fields() ); + feature.setAttribute( QStringLiteral( "key" ), i + 1 ); + feature.setAttribute( QStringLiteral( "label" ), labels.at( i ) ); + feature.setAttribute( QStringLiteral( "category" ), i < 2 ? 1 : 2 ); + features << feature; + } + lookupLayer->dataProvider()->addFeatures( features ); + QgsProject::instance()->addMapLayer( lookupLayer ); + + // create the form layer, that provides fields for building form-scope features + QgsVectorLayer *formLayer = new QgsVectorLayer( + QStringLiteral( "None?field=cat:integer" ), + QStringLiteral( "vr_form" ), + QStringLiteral( "memory" ) + ); + QVERIFY( formLayer && formLayer->isValid() ); + QgsProject::instance()->addMapLayer( formLayer ); + + const QVariantMap baseConfig = + { + { QStringLiteral( "Layer" ), lookupLayer->id() }, + { QStringLiteral( "Key" ), QStringLiteral( "key" ) }, + { QStringLiteral( "Value" ), QStringLiteral( "label" ) }, + { QStringLiteral( "AllowMulti" ), false } + }; -// // Invalidation: helper with no FilterExpression must NOT emit invalidate -// // (the "another" layer config has no filter expression) -// QSignalSpy helperInvalidateSpy( &anotherHelper, &ValueRelationController::invalidate ); -// QSignalSpy helperResultSpy( &anotherHelper, &ValueRelationController::displayValuesReady ); -// anotherHelper.lookupDisplayValues( QStringLiteral( "NONEXISTENT_KEY" ) ); -// QVERIFY( helperResultSpy.wait() ); -// QVERIFY( helperInvalidateSpy.isEmpty() ); -// } + QVariantMap filterConfig = baseConfig; + filterConfig[ QStringLiteral( "FilterExpression" ) ] = + QStringLiteral( "\"category\" = current_value('cat')" ); + + ValueRelationController baseController; + baseController.setConfig( baseConfig ); + + QSignalSpy rawSpy( &baseController, &ValueRelationController::presentRawValue ); + QSignalSpy invalidateSpy( &baseController, &ValueRelationController::invalidateSelection ); + QSignalSpy displaySpy( &baseController, &ValueRelationController::displayTextChanged ); + + // 1. Missing key, no filter -> presentRawValue; NOT invalidateSelection + baseController.lookupDisplayTextOnValueChanged( QStringLiteral( "99999" ) ); + QVERIFY( rawSpy.wait( 5000 ) ); + QVERIFY( displaySpy.isEmpty() ); // mDisplayText was already ""; setDisplayText skips emission + QVERIFY( invalidateSpy.isEmpty() ); + QCOMPARE( baseController.displayText(), QString() ); + + // 2. Hotreload without FilterExpression -> early return, no signals + rawSpy.clear(); + displaySpy.clear(); + baseController.lookupDisplayTextOnHotreload( QStringLiteral( "1" ), QgsFeature() ); + QVERIFY( displaySpy.isEmpty() ); + + // 3. Basic lookup: key "1" -> "Cat1-A" + displaySpy.clear(); + baseController.lookupDisplayTextOnValueChanged( QStringLiteral( "1" ) ); + QVERIFY( displaySpy.wait( 5000 ) ); + QCOMPARE( baseController.displayText(), QStringLiteral( "Cat1-A" ) ); + + ValueRelationController filterController; + filterController.setConfig( filterConfig ); + + QSignalSpy filterDisplaySpy( &filterController, &ValueRelationController::displayTextChanged ); + QSignalSpy filterInvalidateSpy( &filterController, &ValueRelationController::invalidateSelection ); + + // 4. Hotreload with filter, key valid in context -> displayText updated + // Form context: cat=1 -> key 1 ("Cat1-A") is reachable (category=1) + QgsFeature formFeature( formLayer->fields() ); + formFeature.setAttribute( QStringLiteral( "cat" ), 1 ); + formFeature.setValid( true ); + filterController.lookupDisplayTextOnHotreload( QStringLiteral( "1" ), formFeature ); + QVERIFY( filterDisplaySpy.wait( 5000 ) ); + QCOMPARE( filterController.displayText(), QStringLiteral( "Cat1-A" ) ); + + // 5. Hotreload with filter, key not in context -> invalidateSelection + // Form context: cat=2 -> key 1 ("Cat1-A") is NOT reachable (category=1 ≠ 2) + filterDisplaySpy.clear(); + QgsFeature formFeature2( formLayer->fields() ); + formFeature2.setAttribute( QStringLiteral( "cat" ), 2 ); + formFeature2.setValid( true ); + filterController.lookupDisplayTextOnHotreload( QStringLiteral( "1" ), formFeature2 ); + QVERIFY( filterInvalidateSpy.wait( 5000 ) ); + + QgsProject::instance()->removeAllMapLayers(); +} diff --git a/app/test/testformeditors.h b/app/test/testformeditors.h index e53bc49e5..382eb3eee 100644 --- a/app/test/testformeditors.h +++ b/app/test/testformeditors.h @@ -24,7 +24,8 @@ class TestFormEditors : public QObject void testRelationsEditor(); void testRelationsReferenceEditor(); void testRelationsWidgetPresence(); - void testValueRelationsEditor(); + void testValueRelationConversions(); + void testValueRelationControllerLookup(); }; #endif // TESTFORMEDITORS_H diff --git a/app/test/testmodels.cpp b/app/test/testmodels.cpp index 9be05b5ec..dde7bc7e6 100644 --- a/app/test/testmodels.cpp +++ b/app/test/testmodels.cpp @@ -179,304 +179,248 @@ void TestModels::testLayerFeaturesModelSorted() QCOMPARE( model.data( model.index( 8, 0 ), FeaturesModel::ModelRoles::FeatureId ), 100000000 ); } -void TestModels::testValueRelationFeaturesModel() +void TestModels::testValueRelationOrdering() { - // Tests the drawer model: lazy loading, new KeyColumn/ValueColumn roles, - // filter expressions and search — all without form-pair dependency. - - const QString projectDir = TestUtils::testDataDir() + "/project_value_relations"; - QVERIFY( QgsProject::instance()->read( projectDir + "/proj.qgz" ) ); - - QgsMapLayer *subsubL = QgsProject::instance()->mapLayersByName( QStringLiteral( "subsub" ) ).at( 0 ); - QgsVectorLayer *subsubLayer = static_cast( subsubL ); - QVERIFY( subsubLayer && subsubLayer->isValid() ); + /* Tests the four ordering permutations against a layer whose insertion order + * is neither key-sorted nor label-sorted: + * + * key->label: 1->Alpha 2->Delta 3->Gamma 4->Beta + * + * 1. OrderByKey asc -> 1(Alpha), 2(Delta), 3(Gamma), 4(Beta) + * 2. OrderByKey desc -> 4(Beta), 3(Gamma), 2(Delta), 1(Alpha) + * 3. OrderByValue asc -> 1(Alpha), 4(Beta), 2(Delta), 3(Gamma) + * 4. OrderByField "label" asc -> same as 3, but different code path + */ + QgsProject::instance()->removeAllMapLayers(); + + QgsVectorLayer *layer = TestUtils::createVROrderingLayer(); + QVERIFY( layer && layer->isValid() ); + QCOMPARE( static_cast( layer->featureCount() ), 4 ); + QgsProject::instance()->addMapLayer( layer ); const QVariantMap baseConfig = { - { QStringLiteral( "Layer" ), QStringLiteral( "subsub_df9d0ba0_2ec8_4a2c_9f96_84576e37c126" ) }, - { QStringLiteral( "Key" ), QStringLiteral( "fid" ) }, - { QStringLiteral( "Value" ), QStringLiteral( "Name" ) }, + { QStringLiteral( "Layer" ), layer->id() }, + { QStringLiteral( "Key" ), QStringLiteral( "key" ) }, + { QStringLiteral( "Value" ), QStringLiteral( "label" ) } }; - // ── 1. No auto-load: setConfig must not trigger populate ────────────── - { - ValueRelationFeaturesModel model; - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); - - model.setConfig( baseConfig ); - - // Give the event loop a moment; no async work should have started. - QTest::qWait( 100 ); - QVERIFY( spy.isEmpty() ); - QCOMPARE( model.rowCount(), 0 ); - QCOMPARE( model.layer()->id(), subsubLayer->id() ); - } + ValueRelationFeaturesModel model; + QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); - // ── 2. Explicit populate loads features; KeyColumn / ValueColumn roles ─ + auto keyAt = [&]( int row ) + { + return model.data( model.index( row, 0 ), ValueRelationFeaturesModel::KeyColumn ).toString(); + }; + auto valAt = [&]( int row ) { - ValueRelationFeaturesModel model; - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); + return model.data( model.index( row, 0 ), ValueRelationFeaturesModel::ValueColumn ).toString(); + }; - model.setConfig( baseConfig ); + auto reload = [&]( const QVariantMap & config ) + { + spy.clear(); + model.setConfig( config ); model.reloadFeatures(); - spy.wait(); + while ( spy.count() < 2 ) + QVERIFY( spy.wait( 5000 ) ); + }; - QCOMPARE( model.rowCount(), 9 ); + // 1. OrderByKey ascending + QVariantMap config = baseConfig; + config[ QStringLiteral( "OrderByKey" ) ] = true; + config[ QStringLiteral( "OrderByDescending" ) ] = false; + reload( config ); + QCOMPARE( model.rowCount(), 4 ); + QCOMPARE( keyAt( 0 ), QStringLiteral( "1" ) ); + QCOMPARE( keyAt( 1 ), QStringLiteral( "2" ) ); + QCOMPARE( keyAt( 2 ), QStringLiteral( "3" ) ); + QCOMPARE( keyAt( 3 ), QStringLiteral( "4" ) ); + + // 2. OrderByKey descending + config[ QStringLiteral( "OrderByDescending" ) ] = true; + reload( config ); + QCOMPARE( model.rowCount(), 4 ); + QCOMPARE( keyAt( 0 ), QStringLiteral( "4" ) ); + QCOMPARE( keyAt( 1 ), QStringLiteral( "3" ) ); + QCOMPARE( keyAt( 2 ), QStringLiteral( "2" ) ); + QCOMPARE( keyAt( 3 ), QStringLiteral( "1" ) ); + + // 3. OrderByValue ascending + config = baseConfig; + config[ QStringLiteral( "OrderByValue" ) ] = true; + config[ QStringLiteral( "OrderByDescending" ) ] = false; + reload( config ); + QCOMPARE( model.rowCount(), 4 ); + QCOMPARE( valAt( 0 ), QStringLiteral( "Alpha" ) ); + QCOMPARE( valAt( 1 ), QStringLiteral( "Beta" ) ); + QCOMPARE( valAt( 2 ), QStringLiteral( "Delta" ) ); + QCOMPARE( valAt( 3 ), QStringLiteral( "Gamma" ) ); + + // 4. OrderByField with an explicit field name — same result as 3, different code path + config = baseConfig; + config[ QStringLiteral( "OrderByField" ) ] = true; + config[ QStringLiteral( "OrderByFieldName" ) ] = QStringLiteral( "label" ); + config[ QStringLiteral( "OrderByDescending" ) ] = false; + reload( config ); + QCOMPARE( model.rowCount(), 4 ); + QCOMPARE( valAt( 0 ), QStringLiteral( "Alpha" ) ); + QCOMPARE( valAt( 1 ), QStringLiteral( "Beta" ) ); + QCOMPARE( valAt( 2 ), QStringLiteral( "Delta" ) ); + QCOMPARE( valAt( 3 ), QStringLiteral( "Gamma" ) ); + + QgsProject::instance()->removeAllMapLayers(); +} - // KeyColumn returns the raw key-field attribute - QCOMPARE( model.data( model.index( 0, 0 ), ValueRelationFeaturesModel::KeyColumn ), QVariant( 1 ) ); - QCOMPARE( model.data( model.index( 8, 0 ), ValueRelationFeaturesModel::KeyColumn ), QVariant( 100000000 ) ); +void TestModels::testValueRelationSearch() +{ + /* Tests that search applies only to the value (label) column, not the key. + * + * Layer: + * key=1 label="Alpha" — key contains "1", label does not + * key=2 label="Val1" — label contains "1", key does not + * key=100 label="Gamma" — key contains "1", label does not + * + * Search "1" must return only "Val1" — proving that key=1 and key=100 are + * ignored because buildSearchExpression() filters on the value field only. + */ + QgsProject::instance()->removeAllMapLayers(); + + auto *layer = new QgsVectorLayer( + QStringLiteral( "None?field=key:integer&field=label:string" ), + QStringLiteral( "vr_search" ), + QStringLiteral( "memory" ) + ); + QVERIFY( layer && layer->isValid() ); - // ValueColumn returns the display-label attribute - // (insert order from the fixture: first entry is not "A1" without sorting) - QVERIFY( !model.data( model.index( 0, 0 ), ValueRelationFeaturesModel::ValueColumn ).toString().isEmpty() ); + struct Row { int key; QString label; }; + const QList rows = { {1, QStringLiteral( "Alpha" )}, {2, QStringLiteral( "Val1" )}, {100, QStringLiteral( "Gamma" )} }; + + QgsFeatureList features; + for ( const auto &row : rows ) + { + QgsFeature f( layer->fields() ); + f.setAttribute( QStringLiteral( "key" ), row.key ); + f.setAttribute( QStringLiteral( "label" ), row.label ); + features << f; } + layer->dataProvider()->addFeatures( features ); + QCOMPARE( static_cast( layer->featureCount() ), 3 ); + QgsProject::instance()->addMapLayer( layer ); - // ── 3. OrderByValue sorts by the value field ─────────────────────────── + const QVariantMap config = { - QVariantMap config = baseConfig; - config[ QStringLiteral( "OrderByValue" ) ] = true; + { QStringLiteral( "Layer" ), layer->id() }, + { QStringLiteral( "Key" ), QStringLiteral( "key" ) }, + { QStringLiteral( "Value" ), QStringLiteral( "label" ) }, + { QStringLiteral( "OrderByValue" ), true } + }; - ValueRelationFeaturesModel model; - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); + ValueRelationFeaturesModel model; + model.setConfig( config ); - model.setConfig( config ); - model.reloadFeatures(); - spy.wait(); + QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); + auto waitForReload = [&]() + { + while ( spy.count() < 2 ) + QVERIFY( spy.wait( 5000 ) ); + }; - QCOMPARE( model.rowCount(), 9 ); - QCOMPARE( model.data( model.index( 0, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "A1" ) ); - QCOMPARE( model.data( model.index( 1, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "A2" ) ); - QCOMPARE( model.data( model.index( 8, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "VERYBIG" ) ); - } + // Initial load: all 3 features + model.reloadFeatures(); + waitForReload(); + QCOMPARE( model.rowCount(), 3 ); - // ── 4. Search expression filters loaded results ──────────────────────── - { - QVariantMap config = baseConfig; - config[ QStringLiteral( "OrderByValue" ) ] = true; + // Search "1": only "Val1" matches (value column); key=1 and key=100 are ignored + spy.clear(); + model.setSearchExpression( QStringLiteral( "1" ) ); + waitForReload(); + QCOMPARE( model.rowCount(), 1 ); + QCOMPARE( model.data( model.index( 0, 0 ), ValueRelationFeaturesModel::ValueColumn ).toString(), + QStringLiteral( "Val1" ) ); - ValueRelationFeaturesModel model; - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); + // Clear search: all 3 features again + spy.clear(); + model.setSearchExpression( QString() ); + waitForReload(); + QCOMPARE( model.rowCount(), 3 ); - model.setConfig( config ); - model.reloadFeatures(); - spy.wait(); + // Search with no match + spy.clear(); + model.setSearchExpression( QStringLiteral( "xyz" ) ); + waitForReload(); + QCOMPARE( model.rowCount(), 0 ); - model.setSearchExpression( QStringLiteral( "D" ) ); - spy.wait(); + QgsProject::instance()->removeAllMapLayers(); +} - QCOMPARE( model.rowCount(), 2 ); - QCOMPARE( model.data( model.index( 0, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "D1" ) ); - QCOMPARE( model.data( model.index( 1, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "D2" ) ); - } +void TestModels::testValueRelationHotreload() +{ + /* Tests that calling reloadFeatures() after modifying the underlying layer + * triggers hot reload -> reloading after the drawer is already open + * Also tests auto-reload: LayerFeaturesModel connects featureAdded to populate(), + * so adding a feature during an editing session triggers an async reload. + */ + QgsProject::instance()->removeAllMapLayers(); + + QgsVectorLayer *layer = TestUtils::createVRLookupLayer( 3 ); + QVERIFY( layer && layer->isValid() ); + QgsProject::instance()->addMapLayer( layer ); - // ── 5. Static FilterExpression restricts results ─────────────────────── + const QVariantMap config = { - QVariantMap config = baseConfig; - config[ QStringLiteral( "OrderByValue" ) ] = true; - config[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "subFk = 1" ); + { QStringLiteral( "Layer" ), layer->id() }, + { QStringLiteral( "Key" ), QStringLiteral( "key" ) }, + { QStringLiteral( "Value" ), QStringLiteral( "label" ) }, + { QStringLiteral( "OrderByKey" ), true } + }; - ValueRelationFeaturesModel model; - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); + ValueRelationFeaturesModel model; + model.setConfig( config ); - model.setConfig( config ); - model.reloadFeatures(); - spy.wait(); + QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); + auto waitForReload = [&]() + { + while ( spy.count() < 2 ) + QVERIFY( spy.wait( 5000 ) ); + }; - QCOMPARE( model.rowCount(), 2 ); - QCOMPARE( model.data( model.index( 0, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "A1" ) ); - QCOMPARE( model.data( model.index( 1, 0 ), ValueRelationFeaturesModel::ValueColumn ), QLatin1String( "A2" ) ); - } -} + // Initial load: 3 features + model.reloadFeatures(); + waitForReload(); + QCOMPARE( model.rowCount(), 3 ); -void TestModels::testValueRelationController() {} -// { -// // The conversion helpers are non-static Q_INVOKABLE methods; an unconfigured -// // helper is sufficient since they use no instance state. -// ValueRelationController conv; - -// // ── convertFromQgisFormat ───────────────────────────────────────────── -// // Multi-value: parse QGIS "{...}" wire format -// QCOMPARE( conv.convertFromQgisFormat( QStringLiteral( "{1,2,3}" ), true ), -// QStringList( { QStringLiteral( "1" ), QStringLiteral( "2" ), QStringLiteral( "3" ) } ) ); - -// // Single-value: treat as plain value, not list syntax -// QCOMPARE( conv.convertFromQgisFormat( QVariant( 42 ), false ), -// QStringList( { QStringLiteral( "42" ) } ) ); - -// // Null/empty input produces empty list -// QVERIFY( conv.convertFromQgisFormat( QVariant(), false ).isEmpty() ); -// QVERIFY( conv.convertFromQgisFormat( QVariant(), true ).isEmpty() ); -// QVERIFY( conv.convertFromQgisFormat( QStringLiteral( "" ), true ).isEmpty() ); - -// // Large integer key (no scientific-notation rounding) -// QCOMPARE( conv.convertFromQgisFormat( QStringLiteral( "{100000000}" ), true ), -// QStringList( { QStringLiteral( "100000000" ) } ) ); - -// // ── convertToQgisFormat ─────────────────────────────────────────────── -// QCOMPARE( conv.convertToQgisFormat( { QStringLiteral( "1" ), QStringLiteral( "2" ), QStringLiteral( "3" ) } ), -// QStringLiteral( "{1,2,3}" ) ); -// QCOMPARE( conv.convertToQgisFormat( { QStringLiteral( "42" ) } ), QStringLiteral( "{42}" ) ); -// QCOMPARE( conv.convertToQgisFormat( {} ), QStringLiteral( "{}" ) ); - -// // Round-trip -// { -// const QString original = QStringLiteral( "{7,8,100000000}" ); -// QCOMPARE( conv.convertToQgisFormat( conv.convertFromQgisFormat( original, true ) ), original ); -// } - -// // ── Instance method: lookupDisplayValues ────────────────────────────── -// const QString projectDir = TestUtils::testDataDir() + "/project_value_relations"; -// QVERIFY( QgsProject::instance()->read( projectDir + "/proj.qgz" ) ); - -// // Single-value config (AllowMulti=false, the default) -// const QVariantMap singleConfig = -// { -// { QStringLiteral( "Layer" ), QStringLiteral( "subsub_df9d0ba0_2ec8_4a2c_9f96_84576e37c126" ) }, -// { QStringLiteral( "Key" ), QStringLiteral( "fid" ) }, -// { QStringLiteral( "Value" ), QStringLiteral( "Name" ) }, -// }; - -// // Multi-value config (AllowMulti=true) for {…} wire format -// const QVariantMap multiConfig = -// { -// { QStringLiteral( "Layer" ), QStringLiteral( "subsub_df9d0ba0_2ec8_4a2c_9f96_84576e37c126" ) }, -// { QStringLiteral( "Key" ), QStringLiteral( "fid" ) }, -// { QStringLiteral( "Value" ), QStringLiteral( "Name" ) }, -// { QStringLiteral( "AllowMulti" ), true }, -// }; - -// ValueRelationController helper; -// helper.setConfig( singleConfig ); -// QSignalSpy resultSpy( &helper, &ValueRelationController::displayValuesReady ); - -// // Helper to extract the latest result from the spy -// auto latestResult = [&]() -> QVariantList { -// return resultSpy.last().at( 0 ).toList(); -// }; - -// // Single-value lookup: key 1 → one display label -// helper.lookupDisplayValues( QVariant( 1 ) ); -// QVERIFY( resultSpy.wait() ); -// QCOMPARE( latestResult().size(), 1 ); -// QVERIFY( !latestResult().at( 0 ).toString().isEmpty() ); - -// // Null field value: empty result emitted synchronously (no async work started). -// // QSignalSpy::wait() expects one *new* emission after the call, but this one -// // fires before wait() returns, so check the count directly instead. -// { -// const int countBefore = resultSpy.count(); -// helper.lookupDisplayValues( QVariant() ); -// QCOMPARE( resultSpy.count(), countBefore + 1 ); -// QVERIFY( latestResult().isEmpty() ); -// } - -// // Large FID key does not lose precision -// helper.lookupDisplayValues( QVariant( 100000000 ) ); -// QVERIFY( resultSpy.wait() ); -// QCOMPARE( latestResult().size(), 1 ); -// QCOMPARE( latestResult().at( 0 ).toString(), QStringLiteral( "VERYBIG" ) ); - -// // Multi-value lookup: "{1,2}" parsed with AllowMulti=true → two results -// { -// ValueRelationController multiHelper; -// QSignalSpy multiSpy( &multiHelper, &ValueRelationController::displayValuesReady ); -// multiHelper.setConfig( multiConfig ); -// multiHelper.lookupDisplayValues( QStringLiteral( "{1,2}" ) ); -// QVERIFY( multiSpy.wait() ); -// QCOMPARE( multiSpy.last().at( 0 ).toList().size(), 2 ); -// } -// } - -void TestModels::testValueRelationControllerInvalidation() {} -// { -// // Invalidation must fire when a FilterExpression is present and the lookup -// // returns nothing (value became unavailable due to a context change). -// // Without a FilterExpression, invalidation must NOT fire even for a missing key. - -// const QString projectDir = TestUtils::testDataDir() + "/project_value_relations"; -// QVERIFY( QgsProject::instance()->read( projectDir + "/proj.qgz" ) ); - -// QgsMapLayer *mainL = QgsProject::instance()->mapLayersByName( QStringLiteral( "main" ) ).at( 0 ); -// QgsVectorLayer *mainLayer = static_cast( mainL ); -// QVERIFY( mainLayer && mainLayer->isValid() ); - -// // ── No FilterExpression: no invalidate, even for a bogus key ────────── -// { -// const QVariantMap config = -// { -// { QStringLiteral( "Layer" ), QStringLiteral( "subsub_df9d0ba0_2ec8_4a2c_9f96_84576e37c126" ) }, -// { QStringLiteral( "Key" ), QStringLiteral( "fid" ) }, -// { QStringLiteral( "Value" ), QStringLiteral( "Name" ) }, -// }; - -// ValueRelationController helper; -// QSignalSpy invalidateSpy( &helper, &ValueRelationController::invalidate ); -// QSignalSpy resultSpy( &helper, &ValueRelationController::displayValuesReady ); -// helper.setConfig( config ); - -// helper.lookupDisplayValues( QVariant( 9999 ) ); // key that does not exist -// QVERIFY( resultSpy.wait() ); // wait for async to complete -// QVERIFY( invalidateSpy.isEmpty() ); // must NOT invalidate -// } - -// // ── With FilterExpression: invalidate when value is outside filtered set -// { -// // subFk=1 restricts subsub to only fid 1 and 2. -// // If the stored value is fid=5 (which has subFk=3), the lookup returns nothing -// // → should emit invalidate. -// const QVariantMap config = -// { -// { QStringLiteral( "Layer" ), QStringLiteral( "subsub_df9d0ba0_2ec8_4a2c_9f96_84576e37c126" ) }, -// { QStringLiteral( "Key" ), QStringLiteral( "fid" ) }, -// { QStringLiteral( "Value" ), QStringLiteral( "Name" ) }, -// { QStringLiteral( "FilterExpression" ), QStringLiteral( "subFk = 1" ) }, -// }; - -// ValueRelationController helper; -// QSignalSpy invalidateSpy( &helper, &ValueRelationController::invalidate ); -// QSignalSpy resultSpy( &helper, &ValueRelationController::displayValuesReady ); -// helper.setConfig( config ); - -// // fid=5 is not in the subFk=1 subset → should trigger invalidate -// helper.lookupDisplayValues( QVariant( 5 ) ); -// QVERIFY( resultSpy.wait() ); -// QCOMPARE( invalidateSpy.count(), 1 ); -// } - -// // ── With FilterExpression: no invalidate when value IS in filtered set ─ -// { -// const QVariantMap config = -// { -// { QStringLiteral( "Layer" ), QStringLiteral( "subsub_df9d0ba0_2ec8_4a2c_9f96_84576e37c126" ) }, -// { QStringLiteral( "Key" ), QStringLiteral( "fid" ) }, -// { QStringLiteral( "Value" ), QStringLiteral( "Name" ) }, -// { QStringLiteral( "FilterExpression" ), QStringLiteral( "subFk = 1" ) }, -// }; - -// ValueRelationController helper; -// QSignalSpy spy( &helper, &ValueRelationController::invalidate ); -// helper.setConfig( config ); - -// // From the fixture the "subFk=1" subset contains fid 1 and 2 (A1/A2 or similar). -// // We look up all features with that filter to find a valid key, then use it. -// ValueRelationFeaturesModel probeModel; -// QSignalSpy probeSpy( &probeModel, &LayerFeaturesModel::fetchingResultsChanged ); -// probeModel.setConfig( config ); -// probeModel.reloadFeatures(); -// probeSpy.wait(); -// QVERIFY( probeModel.rowCount() > 0 ); - -// const QVariant validKey = probeModel.data( probeModel.index( 0, 0 ), ValueRelationFeaturesModel::KeyColumn ); - -// QSignalSpy invalidateSpy( &helper, &ValueRelationController::invalidate ); -// QSignalSpy resultSpy( &helper, &ValueRelationController::displayValuesReady ); -// helper.lookupDisplayValues( validKey ); -// QVERIFY( resultSpy.wait() ); -// QVERIFY( !resultSpy.last().at( 0 ).toList().isEmpty() ); -// QVERIFY( invalidateSpy.isEmpty() ); -// } -// } + // Add a feature directly to the provider + QgsFeature newFeature( layer->fields() ); + newFeature.setAttribute( QStringLiteral( "key" ), 4 ); + newFeature.setAttribute( QStringLiteral( "label" ), QStringLiteral( "Label 4" ) ); + QVERIFY( layer->dataProvider()->addFeatures( QgsFeatureList() << newFeature ) ); + + // Manual hot reload: reloadFeatures() must pick up the added feature + spy.clear(); + model.reloadFeatures(); + waitForReload(); + QCOMPARE( model.rowCount(), 4 ); + QCOMPARE( model.data( model.index( 3, 0 ), ValueRelationFeaturesModel::KeyColumn ).toString(), + QStringLiteral( "4" ) ); + + // Auto-reload via featureAdded signal: + // startEditing + addFeature fires featureAdded, which LayerFeaturesModel connects to populate(). + // addFeature and commitChanges can each trigger a populate cycle (2 emissions each); + // wait until fetching has stopped (last emission is false) to avoid reading rowCount mid-flight. + spy.clear(); + layer->startEditing(); + QgsFeature editFeature( layer->fields() ); + editFeature.setAttribute( QStringLiteral( "key" ), 5 ); + editFeature.setAttribute( QStringLiteral( "label" ), QStringLiteral( "Label 5" ) ); + QVERIFY( layer->addFeature( editFeature ) ); + QVERIFY( layer->commitChanges() ); + while ( spy.isEmpty() || spy.last().at( 0 ).toBool() ) + QVERIFY( spy.wait( 5000 ) ); + QCOMPARE( model.rowCount(), 5 ); + + QgsProject::instance()->removeAllMapLayers(); +} void TestModels::testProjectsModel() { diff --git a/app/test/testmodels.h b/app/test/testmodels.h index fa5436655..a6ea309cf 100644 --- a/app/test/testmodels.h +++ b/app/test/testmodels.h @@ -23,9 +23,9 @@ class TestModels : public QObject void testStaticFeaturesModel(); void testLayerFeaturesModel(); void testLayerFeaturesModelSorted(); - void testValueRelationFeaturesModel(); - void testValueRelationController(); - void testValueRelationControllerInvalidation(); + void testValueRelationOrdering(); + void testValueRelationSearch(); + void testValueRelationHotreload(); void testProjectsModel(); void testProjectsProxyModel(); diff --git a/app/test/testutils.cpp b/app/test/testutils.cpp index 3c65c7ccb..96e684201 100644 --- a/app/test/testutils.cpp +++ b/app/test/testutils.cpp @@ -21,6 +21,7 @@ #include "qgsvectorlayer.h" #include "qgsproject.h" +#include "qgsfeature.h" #include "qgslayertree.h" #include "qgslayertreelayer.h" @@ -379,6 +380,61 @@ void TestUtils::testIsValidUrl() QVERIFY( !InputUtils::isValidUrl( "" ) ); // empty url is considered valid by QUrl but not by us } +QgsVectorLayer *TestUtils::createVRLookupLayer( int count ) +{ + auto *layer = new QgsVectorLayer( + QStringLiteral( "None?field=key:integer&field=label:string" ), + QStringLiteral( "vr_lookup" ), + QStringLiteral( "memory" ) + ); + if ( !layer || !layer->isValid() ) + return nullptr; + + QgsFeatureList features; + features.reserve( count ); + for ( int i = 1; i <= count; ++i ) + { + QgsFeature f( layer->fields() ); + f.setAttribute( QStringLiteral( "key" ), i ); + f.setAttribute( QStringLiteral( "label" ), QStringLiteral( "Label %1" ).arg( i ) ); + features << f; + } + layer->dataProvider()->addFeatures( features ); + return layer; +} + +QgsVectorLayer *TestUtils::createVROrderingLayer() +{ + auto *layer = new QgsVectorLayer( + QStringLiteral( "None?field=key:integer&field=label:string" ), + QStringLiteral( "vr_ordering" ), + QStringLiteral( "memory" ) + ); + if ( !layer || !layer->isValid() ) + return nullptr; + + struct Row { int key; QString label; }; + const QList rows = + { + {3, QStringLiteral( "Gamma" )}, + {1, QStringLiteral( "Alpha" )}, + {4, QStringLiteral( "Beta" )}, + {2, QStringLiteral( "Delta" )} + }; + + QgsFeatureList features; + features.reserve( rows.size() ); + for ( const auto &row : rows ) + { + QgsFeature f( layer->fields() ); + f.setAttribute( QStringLiteral( "key" ), row.key ); + f.setAttribute( QStringLiteral( "label" ), row.label ); + features << f; + } + layer->dataProvider()->addFeatures( features ); + return layer; +} + bool TestUtils::testExifPositionMetadataExists( const QString &imageSource ) { if ( !QFileInfo::exists( imageSource ) ) diff --git a/app/test/testutils.h b/app/test/testutils.h index 8643248fa..a5819f10c 100644 --- a/app/test/testutils.h +++ b/app/test/testutils.h @@ -49,6 +49,19 @@ namespace TestUtils QgsProject *loadPlanesTestProject(); + /** + * Creates an in-memory no-geometry lookup layer with \a count features. + * Fields: key (integer), label (string). + */ + QgsVectorLayer *createVRLookupLayer( int count ); + + /** + * Creates an in-memory no-geometry lookup layer for ordering tests. + * Features are inserted in an order that is neither key-sorted nor label-sorted: + * key=3 label="Gamma", key=1 label="Alpha", key=4 label="Beta", key=2 label="Delta" + */ + QgsVectorLayer *createVROrderingLayer(); + /** * Generates files and folders in rootPath based on json structure. * \param structure is a json instance, each object is considered as folder. Each folder can have a key named "files" From 12e94f6922b435322dda13a71fd1be42dd52bc07 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Wed, 10 Jun 2026 10:47:52 +0300 Subject: [PATCH 2/3] Minor changes added order by field descending test case --- app/test/testmodels.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app/test/testmodels.cpp b/app/test/testmodels.cpp index dde7bc7e6..48682ace1 100644 --- a/app/test/testmodels.cpp +++ b/app/test/testmodels.cpp @@ -190,6 +190,7 @@ void TestModels::testValueRelationOrdering() * 2. OrderByKey desc -> 4(Beta), 3(Gamma), 2(Delta), 1(Alpha) * 3. OrderByValue asc -> 1(Alpha), 4(Beta), 2(Delta), 3(Gamma) * 4. OrderByField "label" asc -> same as 3, but different code path + * 5. OrderByField desc -> 3(Gamma), 2(Delta), 4(Beta), 1(Alpha) */ QgsProject::instance()->removeAllMapLayers(); @@ -206,6 +207,8 @@ void TestModels::testValueRelationOrdering() }; ValueRelationFeaturesModel model; + model.setConfig( baseConfig ); + QCOMPARE( model.rowCount(), 0 ); // setConfig should not populate the model QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); auto keyAt = [&]( int row ) @@ -269,6 +272,15 @@ void TestModels::testValueRelationOrdering() QCOMPARE( valAt( 2 ), QStringLiteral( "Delta" ) ); QCOMPARE( valAt( 3 ), QStringLiteral( "Gamma" ) ); + // 5. OrderByField descending + config[ QStringLiteral( "OrderByDescending" ) ] = true; + reload( config ); + QCOMPARE( model.rowCount(), 4 ); + QCOMPARE( valAt( 0 ), QStringLiteral( "Gamma" ) ); + QCOMPARE( valAt( 1 ), QStringLiteral( "Delta" ) ); + QCOMPARE( valAt( 2 ), QStringLiteral( "Beta" ) ); + QCOMPARE( valAt( 3 ), QStringLiteral( "Alpha" ) ); + QgsProject::instance()->removeAllMapLayers(); } @@ -305,7 +317,7 @@ void TestModels::testValueRelationSearch() features << f; } layer->dataProvider()->addFeatures( features ); - QCOMPARE( static_cast( layer->featureCount() ), 3 ); + QCOMPARE( layer->featureCount(), ( long long ) 3 ); QgsProject::instance()->addMapLayer( layer ); const QVariantMap config = @@ -317,8 +329,6 @@ void TestModels::testValueRelationSearch() }; ValueRelationFeaturesModel model; - model.setConfig( config ); - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); auto waitForReload = [&]() { @@ -326,6 +336,8 @@ void TestModels::testValueRelationSearch() QVERIFY( spy.wait( 5000 ) ); }; + model.setConfig( config ); + // Initial load: all 3 features model.reloadFeatures(); waitForReload(); @@ -376,8 +388,6 @@ void TestModels::testValueRelationHotreload() }; ValueRelationFeaturesModel model; - model.setConfig( config ); - QSignalSpy spy( &model, &LayerFeaturesModel::fetchingResultsChanged ); auto waitForReload = [&]() { @@ -385,6 +395,8 @@ void TestModels::testValueRelationHotreload() QVERIFY( spy.wait( 5000 ) ); }; + model.setConfig( config ); + // Initial load: 3 features model.reloadFeatures(); waitForReload(); From 367905ba035b995eea0832e875100b8f4f69e967 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Wed, 10 Jun 2026 10:54:40 +0300 Subject: [PATCH 3/3] Changed comments --- app/test/testformeditors.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/test/testformeditors.cpp b/app/test/testformeditors.cpp index b7fda654c..013b5f5c9 100644 --- a/app/test/testformeditors.cpp +++ b/app/test/testformeditors.cpp @@ -455,12 +455,12 @@ void TestFormEditors::testValueRelationConversions() * and multi values. * * AllowMulti=false: - * QGIS value → keys: "1" → ["1"] - * keys → QGIS value: ["42"] → "42" + * QGIS value -> keys: "1" -> ["1"] + * keys -> QGIS value: ["42"] -> "42" * * AllowMulti=true: - * QGIS value → keys: "{1,3,4}" → ["1","3","4"] - * keys → QGIS value: ["1","3","4"] → "{1,3,4}"; ["1"] → "{1}" + * QGIS value -> keys: "{1,3,4}" -> ["1","3","4"] + * keys -> QGIS value: ["1","3","4"] -> "{1,3,4}"; ["1"] -> "{1}" */ QgsProject::instance()->removeAllMapLayers(); @@ -532,16 +532,16 @@ void TestFormEditors::testValueRelationControllerLookup() /* Tests async display-text lookup for ValueRelationController: * * baseConfig controller (no FilterExpression): - * 1. Missing key → presentRawValue fires; invalidateSelection does NOT fire; displayText="" - * 2. lookupDisplayTextOnHotreload without FilterExpression → returns early, no signals - * 3. Basic lookup: lookupDisplayTextOnValueChanged("1") → "Cat1-A" + * 1. Missing key -> presentRawValue fires; invalidateSelection does NOT fire; displayText="" + * 2. lookupDisplayTextOnHotreload without FilterExpression -> returns early, no signals + * 3. Basic lookup: lookupDisplayTextOnValueChanged("1") -> "Cat1-A" * * filterConfig controller (FilterExpression set): - * 4. lookupDisplayTextOnHotreload, key valid in context → displayText updated ("Cat1-A") - * 5. lookupDisplayTextOnHotreload, key no longer in context → invalidateSelection fires + * 4. lookupDisplayTextOnHotreload, key valid in context -> displayText updated ("Cat1-A") + * 5. lookupDisplayTextOnHotreload, key no longer in context -> invalidateSelection fires * * Cases 1-2 run before case 3 so that mDisplayText is "" when case 1 checks - * displaySpy.isEmpty() — the setDisplayText guard skips emission for equal values. + * displaySpy.isEmpty() -> the setDisplayText guard skips emission for equal values. * * Lookup layer: * key=1 label="Cat1-A" category=1 @@ -550,7 +550,7 @@ void TestFormEditors::testValueRelationControllerLookup() * key=4 label="Cat2-B" category=2 * * Filter expression: "category" = current_value('cat') - * Form feature cat=1 → keys 1,2 reachable; cat=2 → keys 3,4 reachable. + * Form feature cat=1 -> keys 1,2 reachable; cat=2 -> keys 3,4 reachable. */ QgsProject::instance()->removeAllMapLayers();