1 /***************************************************************************
2     testqgsattributeform.cpp
3      --------------------------------------
4     Date                 : 13 05 2016
5     Copyright            : (C) 2016 Paul Blottiere
6     Email                : paul dot blottiere at oslandia dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 
17 #include "qgstest.h"
18 #include <QPushButton>
19 #include <QLineEdit>
20 
21 #include <editorwidgets/core/qgseditorwidgetregistry.h>
22 #include "qgsattributeform.h"
23 #include <qgsapplication.h>
24 #include "qgseditorwidgetwrapper.h"
25 #include <qgsvectorlayer.h>
26 #include "qgsvectordataprovider.h"
27 #include <qgsfeature.h>
28 #include <qgsvectorlayerjoininfo.h>
29 #include "qgsgui.h"
30 #include "qgsattributeformeditorwidget.h"
31 #include "qgsattributeforminterface.h"
32 #include "qgsmultiedittoolbutton.h"
33 #include "qgsattributeeditorfield.h"
34 #include "qgsattributeeditorcontainer.h"
35 #include <QSignalSpy>
36 
37 class TestQgsAttributeForm : public QObject
38 {
39     Q_OBJECT
40   public:
41     TestQgsAttributeForm() = default;
42 
43   private slots:
44     void initTestCase(); // will be called before the first testfunction is executed.
45     void cleanupTestCase(); // will be called after the last testfunction was executed.
46     void init(); // will be called before each testfunction is executed.
47     void cleanup(); // will be called after every testfunction.
48 
49     void testFieldConstraint();
50     void testFieldMultiConstraints();
51     void testOKButtonStatus();
52     void testDynamicForm();
53     void testConstraintsOnJoinedFields();
54     void testEditableJoin();
55     void testUpsertOnEdit();
56     void testFixAttributeForm();
57     void testAttributeFormInterface();
58     void testDefaultValueUpdate();
59     void testDefaultValueUpdateRecursion();
60     void testSameFieldSync();
61     void testZeroDoubles();
62 
63   private:
constraintsLabel(QgsAttributeForm * form,QgsEditorWidgetWrapper * ww)64     QLabel *constraintsLabel( QgsAttributeForm *form, QgsEditorWidgetWrapper *ww )
65     {
66       QgsAttributeFormEditorWidget *formEditorWidget = form->mFormEditorWidgets.value( ww->fieldIdx() );
67       return formEditorWidget->findChild<QLabel *>( QStringLiteral( "ConstraintStatus" ) );
68     }
69 };
70 
initTestCase()71 void TestQgsAttributeForm::initTestCase()
72 {
73   QgsApplication::init();
74   QgsApplication::initQgis();
75   QgsGui::editorWidgetRegistry()->initEditors();
76 }
77 
cleanupTestCase()78 void TestQgsAttributeForm::cleanupTestCase()
79 {
80   QgsApplication::exitQgis();
81 }
82 
init()83 void TestQgsAttributeForm::init()
84 {
85 }
86 
cleanup()87 void TestQgsAttributeForm::cleanup()
88 {
89 }
90 
testFieldConstraint()91 void TestQgsAttributeForm::testFieldConstraint()
92 {
93   // make a temporary vector layer
94   const QString def = QStringLiteral( "Point?field=col0:integer" );
95   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
96   layer->setEditorWidgetSetup( 0, QgsEditorWidgetSetup( QStringLiteral( "TextEdit" ), QVariantMap() ) );
97 
98   // add a feature to the vector layer
99   QgsFeature ft( layer->dataProvider()->fields(), 1 );
100   ft.setAttribute( QStringLiteral( "col0" ), 0 );
101 
102   // build a form for this feature
103   QgsAttributeForm form( layer );
104   form.setFeature( ft );
105 
106   // testing stuff
107   const QString validLabel = QStringLiteral( "<font color=\"#259B24\">%1</font>" ).arg( QChar( 0x2714 ) );
108   const QString invalidLabel = QStringLiteral( "<font color=\"#FF9800\">%1</font>" ).arg( QChar( 0x2718 ) );
109   const QString warningLabel = QStringLiteral( "<font color=\"#FFC107\">%1</font>" ).arg( QChar( 0x2718 ) );
110 
111   // set constraint
112   layer->setConstraintExpression( 0, QString() );
113 
114   // get wrapper
115   QgsEditorWidgetWrapper *ww = nullptr;
116   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
117 
118   // no constraint so we expect an empty label
119   QCOMPARE( constraintsLabel( &form, ww )->text(), QString() );
120 
121   // set a not null constraint
122   layer->setConstraintExpression( 0, QStringLiteral( "col0 is not null" ) );
123   // build a form for this feature
124   QgsAttributeForm form2( layer );
125   form2.setFeature( ft );
126   QSignalSpy spy( &form2, SIGNAL( widgetValueChanged( QString, QVariant, bool ) ) );
127   ww = qobject_cast<QgsEditorWidgetWrapper *>( form2.mWidgets[0] );
128 
129   // set value to 1
130   ww->setValues( 1, QVariantList() );
131   QCOMPARE( spy.count(), 1 );
132   QCOMPARE( constraintsLabel( &form2, ww )->text(), validLabel );
133 
134   // set value to null
135   spy.clear();
136   ww->setValues( QVariant(), QVariantList() );
137   QCOMPARE( spy.count(), 1 );
138   QCOMPARE( constraintsLabel( &form2, ww )->text(), invalidLabel );
139 
140   // set value to 1
141   spy.clear();
142   ww->setValues( 1, QVariantList() );
143   QCOMPARE( spy.count(), 1 );
144   QCOMPARE( constraintsLabel( &form2, ww )->text(), validLabel );
145 
146   // set a soft constraint
147   layer->setConstraintExpression( 0, QStringLiteral( "col0 is not null" ) );
148   layer->setFieldConstraint( 0, QgsFieldConstraints::ConstraintExpression, QgsFieldConstraints::ConstraintStrengthSoft );
149   // build a form for this feature
150   QgsAttributeForm form3( layer );
151   form3.setFeature( ft );
152   ww = qobject_cast<QgsEditorWidgetWrapper *>( form3.mWidgets[0] );
153 
154   // set value to 1
155   ww->setValues( 1, QVariantList() );
156   QCOMPARE( constraintsLabel( &form3, ww )->text(), validLabel );
157 
158   // set value to null
159   ww->setValues( QVariant(), QVariantList() );
160   QCOMPARE( constraintsLabel( &form3, ww )->text(), warningLabel );
161 
162   // set value to 1
163   ww->setValues( 1, QVariantList() );
164   QCOMPARE( constraintsLabel( &form3, ww )->text(), validLabel );
165 }
166 
testFieldMultiConstraints()167 void TestQgsAttributeForm::testFieldMultiConstraints()
168 {
169   // make a temporary layer to check through
170   const QString def = QStringLiteral( "Point?field=col0:integer&field=col1:integer&field=col2:integer&field=col3:integer" );
171   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
172 
173   // add features to the vector layer
174   QgsFeature ft( layer->dataProvider()->fields(), 1 );
175   ft.setAttribute( QStringLiteral( "col0" ), 0 );
176   ft.setAttribute( QStringLiteral( "col1" ), 1 );
177   ft.setAttribute( QStringLiteral( "col2" ), 2 );
178   ft.setAttribute( QStringLiteral( "col3" ), 3 );
179 
180   // set constraints for each field
181   layer->setConstraintExpression( 0, QString() );
182   layer->setConstraintExpression( 1, QString() );
183   layer->setConstraintExpression( 2, QString() );
184   layer->setConstraintExpression( 3, QString() );
185 
186   // build a form for this feature
187   QgsAttributeForm form( layer );
188   form.setFeature( ft );
189 
190   // testing stuff
191   const QSignalSpy spy( &form, SIGNAL( attributeChanged( QString, QVariant ) ) );
192   const QString val = QStringLiteral( "<font color=\"#259B24\">%1</font>" ).arg( QChar( 0x2714 ) );
193   const QString inv = QStringLiteral( "<font color=\"#FF9800\">%1</font>" ).arg( QChar( 0x2718 ) );
194 
195   // get wrappers for each widget
196   QgsEditorWidgetWrapper *ww0, *ww1, *ww2, *ww3;
197   ww0 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
198   ww1 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
199   ww2 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
200   ww3 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[3] );
201 
202   // no constraint so we expect an empty label
203   QVERIFY( constraintsLabel( &form, ww0 )->text().isEmpty() );
204   QVERIFY( constraintsLabel( &form, ww1 )->text().isEmpty() );
205   QVERIFY( constraintsLabel( &form, ww2 )->text().isEmpty() );
206   QVERIFY( constraintsLabel( &form, ww3 )->text().isEmpty() );
207 
208   // update constraint
209   layer->setConstraintExpression( 0, QStringLiteral( "col0 < (col1 * col2)" ) );
210   layer->setConstraintExpression( 1, QString() );
211   layer->setConstraintExpression( 2, QString() );
212   layer->setConstraintExpression( 3, QStringLiteral( "col0 = 2" ) );
213 
214   QgsAttributeForm form2( layer );
215   form2.setFeature( ft );
216   ww0 = qobject_cast<QgsEditorWidgetWrapper *>( form2.mWidgets[0] );
217   ww1 = qobject_cast<QgsEditorWidgetWrapper *>( form2.mWidgets[1] );
218   ww2 = qobject_cast<QgsEditorWidgetWrapper *>( form2.mWidgets[2] );
219   ww3 = qobject_cast<QgsEditorWidgetWrapper *>( form2.mWidgets[3] );
220   QSignalSpy spy2( &form2, SIGNAL( widgetValueChanged( QString, QVariant, bool ) ) );
221 
222   // change value
223   ww0->setValues( 2, QVariantList() ); // update col0
224   QCOMPARE( spy2.count(), 1 );
225 
226   QCOMPARE( constraintsLabel( &form2, ww0 )->text(), inv ); // 2 < ( 1 + 2 )
227   QCOMPARE( constraintsLabel( &form2, ww1 )->text(), QString() );
228   QCOMPARE( constraintsLabel( &form2, ww2 )->text(), QString() );
229   QCOMPARE( constraintsLabel( &form2, ww3 )->text(), val ); // 2 = 2
230 
231   // change value
232   spy2.clear();
233   ww0->setValues( 1, QVariantList() ); // update col0
234   QCOMPARE( spy2.count(), 1 );
235 
236   QCOMPARE( constraintsLabel( &form2, ww0 )->text(), val ); // 1 < ( 1 + 2 )
237   QCOMPARE( constraintsLabel( &form2, ww1 )->text(), QString() );
238   QCOMPARE( constraintsLabel( &form2, ww2 )->text(), QString() );
239   QCOMPARE( constraintsLabel( &form2, ww3 )->text(), inv ); // 2 = 1
240 }
241 
testOKButtonStatus()242 void TestQgsAttributeForm::testOKButtonStatus()
243 {
244   // make a temporary vector layer
245   const QString def = QStringLiteral( "Point?field=col0:integer" );
246   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
247 
248   // add a feature to the vector layer
249   QgsFeature ft( layer->dataProvider()->fields(), 1 );
250   ft.setAttribute( QStringLiteral( "col0" ), 0 );
251   ft.setValid( true );
252 
253   // set constraint
254   layer->setConstraintExpression( 0, QString() );
255 
256   // build a form for this feature
257   QgsAttributeForm form( layer );
258   form.setFeature( ft );
259 
260   QPushButton *okButton = form.mButtonBox->button( QDialogButtonBox::Ok );
261 
262   // get wrapper
263   QgsEditorWidgetWrapper *ww = nullptr;
264   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
265 
266   // testing stuff
267   const QSignalSpy spy1( &form, SIGNAL( attributeChanged( QString, QVariant ) ) );
268   const QSignalSpy spy2( layer, SIGNAL( editingStarted() ) );
269   const QSignalSpy spy3( layer, SIGNAL( editingStopped() ) );
270 
271   // no constraint but layer not editable : OK button disabled
272   QCOMPARE( layer->isEditable(), false );
273   QCOMPARE( okButton->isEnabled(), false );
274 
275   // no constraint and editable layer : OK button enabled
276   layer->startEditing();
277   QCOMPARE( spy2.count(), 1 );
278   QCOMPARE( layer->isEditable(), true );
279   QCOMPARE( okButton->isEnabled(), true );
280 
281   // invalid constraint and editable layer : OK button disabled
282   layer->setConstraintExpression( 0, QStringLiteral( "col0 = 0" ) );
283   QgsAttributeForm form2( layer );
284   form2.setFeature( ft );
285   ww = qobject_cast<QgsEditorWidgetWrapper *>( form2.mWidgets[0] );
286   okButton = form2.mButtonBox->button( QDialogButtonBox::Ok );
287   ww->setValues( 1, QVariantList() );
288   QCOMPARE( okButton->isEnabled(), false );
289 
290   // valid constraint and editable layer : OK button enabled
291   layer->setConstraintExpression( 0, QStringLiteral( "col0 = 2" ) );
292   QgsAttributeForm form3( layer );
293   form3.setFeature( ft );
294   ww = qobject_cast<QgsEditorWidgetWrapper *>( form3.mWidgets[0] );
295   okButton = form3.mButtonBox->button( QDialogButtonBox::Ok );
296 
297   ww->setValues( 2, QVariantList() );
298   QCOMPARE( okButton->isEnabled(), true );
299 
300   // valid constraint and not editable layer : OK button disabled
301   layer->rollBack();
302   QCOMPARE( spy3.count(), 1 );
303   QCOMPARE( layer->isEditable(), false );
304   QCOMPARE( okButton->isEnabled(), false );
305 
306   // set soft constraint
307   layer->setFieldConstraint( 0, QgsFieldConstraints::ConstraintExpression, QgsFieldConstraints::ConstraintStrengthSoft );
308   QgsAttributeForm form4( layer );
309   form4.setFeature( ft );
310   ww = qobject_cast<QgsEditorWidgetWrapper *>( form4.mWidgets[0] );
311   okButton = form4.mButtonBox->button( QDialogButtonBox::Ok );
312   ww->setValues( 1, QVariantList() );
313   QVERIFY( !okButton->isEnabled() );
314   layer->startEditing();
315   // just a soft constraint, so OK should be enabled
316   QVERIFY( okButton->isEnabled() );
317   layer->rollBack();
318   QVERIFY( !okButton->isEnabled() );
319 }
320 
testDynamicForm()321 void TestQgsAttributeForm::testDynamicForm()
322 {
323   // make temporary layers
324   const QString defA = QStringLiteral( "Point?field=id_a:integer" );
325   QgsVectorLayer *layerA = new QgsVectorLayer( defA, QStringLiteral( "layerA" ), QStringLiteral( "memory" ) );
326 
327   const QString defB = QStringLiteral( "Point?field=id_b:integer&field=col0:integer" );
328   QgsVectorLayer *layerB = new QgsVectorLayer( defB, QStringLiteral( "layerB" ), QStringLiteral( "memory" ) );
329 
330   const QString defC = QStringLiteral( "Point?field=id_c:integer&field=col0:integer" );
331   QgsVectorLayer *layerC = new QgsVectorLayer( defC, QStringLiteral( "layerC" ), QStringLiteral( "memory" ) );
332 
333   // join configuration
334   QgsVectorLayerJoinInfo infoJoinAB;
335   infoJoinAB.setTargetFieldName( QStringLiteral( "id_a" ) );
336   infoJoinAB.setJoinLayer( layerB );
337   infoJoinAB.setJoinFieldName( QStringLiteral( "id_b" ) );
338   infoJoinAB.setDynamicFormEnabled( true );
339 
340   layerA->addJoin( infoJoinAB );
341 
342   QgsVectorLayerJoinInfo infoJoinAC;
343   infoJoinAC.setTargetFieldName( QStringLiteral( "id_a" ) );
344   infoJoinAC.setJoinLayer( layerC );
345   infoJoinAC.setJoinFieldName( QStringLiteral( "id_c" ) );
346   infoJoinAC.setDynamicFormEnabled( true );
347 
348   layerA->addJoin( infoJoinAC );
349 
350   // add features for main layer
351   QgsFeature ftA( layerA->fields() );
352   ftA.setAttribute( QStringLiteral( "id_a" ), 0 );
353   layerA->startEditing();
354   layerA->addFeature( ftA );
355   layerA->commitChanges();
356 
357   // add features for joined layers
358   QgsFeature ft0B( layerB->fields() );
359   ft0B.setAttribute( QStringLiteral( "id_b" ), 30 );
360   ft0B.setAttribute( QStringLiteral( "col0" ), 10 );
361   layerB->startEditing();
362   layerB->addFeature( ft0B );
363   layerB->commitChanges();
364 
365   QgsFeature ft1B( layerB->fields() );
366   ft1B.setAttribute( QStringLiteral( "id_b" ), 31 );
367   ft1B.setAttribute( QStringLiteral( "col0" ), 11 );
368   layerB->startEditing();
369   layerB->addFeature( ft1B );
370   layerB->commitChanges();
371 
372   QgsFeature ft0C( layerC->fields() );
373   ft0C.setAttribute( QStringLiteral( "id_c" ), 32 );
374   ft0C.setAttribute( QStringLiteral( "col0" ), 12 );
375   layerC->startEditing();
376   layerC->addFeature( ft0C );
377   layerC->commitChanges();
378 
379   QgsFeature ft1C( layerC->fields() );
380   ft1C.setAttribute( QStringLiteral( "id_c" ), 31 );
381   ft1C.setAttribute( QStringLiteral( "col0" ), 13 );
382   layerC->startEditing();
383   layerC->addFeature( ft1C );
384   layerC->commitChanges();
385 
386   // build a form with feature A
387   QgsAttributeForm form( layerA );
388   form.setMode( QgsAttributeEditorContext::AddFeatureMode );
389   form.setFeature( ftA );
390 
391   // test that there's no joined feature by default
392   QgsEditorWidgetWrapper *ww = nullptr;
393 
394   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
395   QCOMPARE( ww->field().name(), QString( "layerB_col0" ) );
396   QCOMPARE( ww->value(), QVariant( QVariant::Int ) );
397 
398   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
399   QCOMPARE( ww->field().name(), QString( "layerC_col0" ) );
400   QCOMPARE( ww->value(), QVariant( QVariant::Int ) );
401 
402   // change layerA join id field to join with layerB
403   form.changeAttribute( QStringLiteral( "id_a" ), QVariant( 30 ) );
404 
405   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
406   QCOMPARE( ww->field().name(), QString( "id_a" ) );
407   QCOMPARE( ww->value(), QVariant( 30 ) );
408 
409   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
410   QCOMPARE( ww->field().name(), QString( "layerB_col0" ) );
411   QCOMPARE( ww->value(), QVariant( 10 ) );
412 
413   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
414   QCOMPARE( ww->field().name(), QString( "layerC_col0" ) );
415   QCOMPARE( ww->value(), QVariant( QVariant::Int ) );
416 
417   // change layerA join id field to join with layerC
418   form.changeAttribute( QStringLiteral( "id_a" ), QVariant( 32 ) );
419 
420   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
421   QCOMPARE( ww->field().name(), QString( "id_a" ) );
422   QCOMPARE( ww->value(), QVariant( 32 ) );
423 
424   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
425   QCOMPARE( ww->field().name(), QString( "layerB_col0" ) );
426   QCOMPARE( ww->value(), QVariant( QVariant::Int ) );
427 
428   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
429   QCOMPARE( ww->field().name(), QString( "layerC_col0" ) );
430   QCOMPARE( ww->value(), QVariant( 12 ) );
431 
432   // change layerA join id field to join with layerA and layerC
433   form.changeAttribute( QStringLiteral( "id_a" ), QVariant( 31 ) );
434 
435   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
436   QCOMPARE( ww->field().name(), QString( "id_a" ) );
437   QCOMPARE( ww->value(), QVariant( 31 ) );
438 
439   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
440   QCOMPARE( ww->field().name(), QString( "layerB_col0" ) );
441   QCOMPARE( ww->value(), QVariant( 11 ) );
442 
443   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
444   QCOMPARE( ww->field().name(), QString( "layerC_col0" ) );
445   QCOMPARE( ww->value(), QVariant( 13 ) );
446 
447   // clean
448   delete layerA;
449   delete layerB;
450   delete layerC;
451 }
452 
testConstraintsOnJoinedFields()453 void TestQgsAttributeForm::testConstraintsOnJoinedFields()
454 {
455   const QString validLabel = QStringLiteral( "<font color=\"#259B24\">%1</font>" ).arg( QChar( 0x2714 ) );
456   const QString warningLabel = QStringLiteral( "<font color=\"#FFC107\">%1</font>" ).arg( QChar( 0x2718 ) );
457 
458   // make temporary layers
459   const QString defA = QStringLiteral( "Point?field=id_a:integer" );
460   QgsVectorLayer *layerA = new QgsVectorLayer( defA, QStringLiteral( "layerA" ), QStringLiteral( "memory" ) );
461 
462   const QString defB = QStringLiteral( "Point?field=id_b:integer&field=col0:integer" );
463   QgsVectorLayer *layerB = new QgsVectorLayer( defB, QStringLiteral( "layerB" ), QStringLiteral( "memory" ) );
464 
465   // set constraints on joined layer
466   layerB->setConstraintExpression( 1, QStringLiteral( "col0 < 10" ) );
467   layerB->setFieldConstraint( 1, QgsFieldConstraints::ConstraintExpression, QgsFieldConstraints::ConstraintStrengthSoft );
468 
469   // join configuration
470   QgsVectorLayerJoinInfo infoJoinAB;
471   infoJoinAB.setTargetFieldName( QStringLiteral( "id_a" ) );
472   infoJoinAB.setJoinLayer( layerB );
473   infoJoinAB.setJoinFieldName( QStringLiteral( "id_b" ) );
474   infoJoinAB.setDynamicFormEnabled( true );
475 
476   layerA->addJoin( infoJoinAB );
477 
478   // add features for main layer
479   QgsFeature ftA( layerA->fields() );
480   ftA.setAttribute( QStringLiteral( "id_a" ), 1 );
481   layerA->startEditing();
482   layerA->addFeature( ftA );
483   layerA->commitChanges();
484 
485   // add features for joined layer
486   QgsFeature ft0B( layerB->fields() );
487   ft0B.setAttribute( QStringLiteral( "id_b" ), 30 );
488   ft0B.setAttribute( QStringLiteral( "col0" ), 9 );
489   layerB->startEditing();
490   layerB->addFeature( ft0B );
491   layerB->commitChanges();
492 
493   QgsFeature ft1B( layerB->fields() );
494   ft1B.setAttribute( QStringLiteral( "id_b" ), 31 );
495   ft1B.setAttribute( QStringLiteral( "col0" ), 11 );
496   layerB->startEditing();
497   layerB->addFeature( ft1B );
498   layerB->commitChanges();
499 
500   // build a form for this feature
501   QgsAttributeForm form( layerA );
502   form.setMode( QgsAttributeEditorContext::AddFeatureMode );
503   form.setFeature( ftA );
504 
505   // change layerA join id field
506   form.changeAttribute( QStringLiteral( "id_a" ), QVariant( 30 ) );
507 
508   // compare
509   QgsEditorWidgetWrapper *ww = nullptr;
510   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
511   QCOMPARE( constraintsLabel( &form, ww )->text(), validLabel );
512 
513   // change layerA join id field
514   form.changeAttribute( QStringLiteral( "id_a" ), QVariant( 31 ) );
515 
516   // compare
517   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
518   QCOMPARE( constraintsLabel( &form, ww )->text(), warningLabel );
519 }
520 
testEditableJoin()521 void TestQgsAttributeForm::testEditableJoin()
522 {
523   // make temporary layers
524   const QString defA = QStringLiteral( "Point?field=id_a:integer" );
525   QgsVectorLayer *layerA = new QgsVectorLayer( defA, QStringLiteral( "layerA" ), QStringLiteral( "memory" ) );
526 
527   const QString defB = QStringLiteral( "Point?field=id_b:integer&field=col0:integer" );
528   QgsVectorLayer *layerB = new QgsVectorLayer( defB, QStringLiteral( "layerB" ), QStringLiteral( "memory" ) );
529 
530   const QString defC = QStringLiteral( "Point?field=id_c:integer&field=col0:integer" );
531   QgsVectorLayer *layerC = new QgsVectorLayer( defC, QStringLiteral( "layerC" ), QStringLiteral( "memory" ) );
532 
533   // join configuration
534   QgsVectorLayerJoinInfo infoJoinAB;
535   infoJoinAB.setTargetFieldName( QStringLiteral( "id_a" ) );
536   infoJoinAB.setJoinLayer( layerB );
537   infoJoinAB.setJoinFieldName( QStringLiteral( "id_b" ) );
538   infoJoinAB.setDynamicFormEnabled( true );
539   infoJoinAB.setEditable( true );
540 
541   layerA->addJoin( infoJoinAB );
542 
543   QgsVectorLayerJoinInfo infoJoinAC;
544   infoJoinAC.setTargetFieldName( QStringLiteral( "id_a" ) );
545   infoJoinAC.setJoinLayer( layerC );
546   infoJoinAC.setJoinFieldName( QStringLiteral( "id_c" ) );
547   infoJoinAC.setDynamicFormEnabled( true );
548   infoJoinAC.setEditable( false );
549 
550   layerA->addJoin( infoJoinAC );
551 
552   // add features for main layer
553   QgsFeature ftA( layerA->fields() );
554   ftA.setAttribute( QStringLiteral( "id_a" ), 31 );
555   layerA->startEditing();
556   layerA->addFeature( ftA );
557   layerA->commitChanges();
558 
559   // add features for joined layers
560   QgsFeature ft0B( layerB->fields() );
561   ft0B.setAttribute( QStringLiteral( "id_b" ), 31 );
562   ft0B.setAttribute( QStringLiteral( "col0" ), 11 );
563   layerB->startEditing();
564   layerB->addFeature( ft0B );
565   layerB->commitChanges();
566 
567   QgsFeature ft0C( layerC->fields() );
568   ft0C.setAttribute( QStringLiteral( "id_c" ), 31 );
569   ft0C.setAttribute( QStringLiteral( "col0" ), 13 );
570   layerC->startEditing();
571   layerC->addFeature( ft0C );
572   layerC->commitChanges();
573 
574   // start editing layers
575   layerA->startEditing();
576   layerB->startEditing();
577   layerC->startEditing();
578 
579   // build a form with feature A
580   ftA = layerA->getFeature( 1 );
581 
582   QgsAttributeForm form( layerA );
583   form.setMode( QgsAttributeEditorContext::SingleEditMode );
584   form.setFeature( ftA );
585 
586   // change layerA join id field to join with layerB and layerC
587   QgsEditorWidgetWrapper *ww = nullptr;
588 
589   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
590   QCOMPARE( ww->field().name(), QString( "id_a" ) );
591   QCOMPARE( ww->value(), QVariant( 31 ) );
592 
593   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
594   QCOMPARE( ww->field().name(), QString( "layerB_col0" ) );
595   QCOMPARE( ww->value(), QVariant( 11 ) );
596 
597   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
598   QCOMPARE( ww->field().name(), QString( "layerC_col0" ) );
599   QCOMPARE( ww->value(), QVariant( 13 ) );
600 
601   // test if widget is enabled for layerA
602   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
603   QCOMPARE( ww->widget()->isEnabled(), true );
604 
605   // test if widget is enabled for layerB
606   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
607   QCOMPARE( ww->widget()->isEnabled(), true );
608 
609   // test if widget is disabled for layerC
610   ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
611   QCOMPARE( ww->widget()->isEnabled(), false );
612 
613   // change attributes
614   form.changeAttribute( QStringLiteral( "layerB_col0" ), QVariant( 333 ) );
615   form.changeAttribute( QStringLiteral( "layerC_col0" ), QVariant( 444 ) );
616   form.save();
617 
618   // commit changes
619   layerA->commitChanges();
620   layerB->commitChanges();
621   layerC->commitChanges();
622 
623   // check attributes
624   ft0B = layerB->getFeature( 1 );
625   QCOMPARE( ft0B.attribute( "col0" ), QVariant( 333 ) );
626 
627   ft0C = layerC->getFeature( 1 );
628   QCOMPARE( ft0C.attribute( "col0" ), QVariant( 13 ) );
629 
630   // all editor widget must have a multi edit button
631   layerA->startEditing();
632   layerB->startEditing();
633   layerC->startEditing();
634   layerA->select( ftA.id() );
635   form.setMode( QgsAttributeEditorContext::MultiEditMode );
636 
637   // multi edit button must be displayed for A
638   QgsAttributeFormEditorWidget *formWidget = qobject_cast<QgsAttributeFormEditorWidget *>( form.mFormWidgets[1] );
639   QVERIFY( formWidget->mMultiEditButton->parent() );
640 
641   // multi edit button must be displayed for B (join is editable)
642   formWidget = qobject_cast<QgsAttributeFormEditorWidget *>( form.mFormWidgets[1] );
643   QVERIFY( formWidget->mMultiEditButton->parent() );
644 
645   // multi edit button must not be displayed for C (join is not editable)
646   formWidget = qobject_cast<QgsAttributeFormEditorWidget *>( form.mFormWidgets[2] );
647   QVERIFY( !formWidget->mMultiEditButton->parent() );
648 
649   // clean
650   delete layerA;
651   delete layerB;
652   delete layerC;
653 }
654 
testUpsertOnEdit()655 void TestQgsAttributeForm::testUpsertOnEdit()
656 {
657   // make temporary layers
658   const QString defA = QStringLiteral( "Point?field=id_a:integer" );
659   QgsVectorLayer *layerA = new QgsVectorLayer( defA, QStringLiteral( "layerA" ), QStringLiteral( "memory" ) );
660 
661   const QString defB = QStringLiteral( "Point?field=id_b:integer&field=col0:integer" );
662   QgsVectorLayer *layerB = new QgsVectorLayer( defB, QStringLiteral( "layerB" ), QStringLiteral( "memory" ) );
663 
664   const QString defC = QStringLiteral( "Point?field=id_c:integer&field=col0:integer" );
665   QgsVectorLayer *layerC = new QgsVectorLayer( defC, QStringLiteral( "layerC" ), QStringLiteral( "memory" ) );
666 
667   // join configuration
668   QgsVectorLayerJoinInfo infoJoinAB;
669   infoJoinAB.setTargetFieldName( QStringLiteral( "id_a" ) );
670   infoJoinAB.setJoinLayer( layerB );
671   infoJoinAB.setJoinFieldName( QStringLiteral( "id_b" ) );
672   infoJoinAB.setDynamicFormEnabled( true );
673   infoJoinAB.setEditable( true );
674   infoJoinAB.setUpsertOnEdit( true );
675 
676   layerA->addJoin( infoJoinAB );
677 
678   QgsVectorLayerJoinInfo infoJoinAC;
679   infoJoinAC.setTargetFieldName( QStringLiteral( "id_a" ) );
680   infoJoinAC.setJoinLayer( layerC );
681   infoJoinAC.setJoinFieldName( QStringLiteral( "id_c" ) );
682   infoJoinAC.setDynamicFormEnabled( true );
683   infoJoinAC.setEditable( true );
684   infoJoinAC.setUpsertOnEdit( false );
685 
686   layerA->addJoin( infoJoinAC );
687 
688   // add features for main layer
689   QgsFeature ft0A( layerA->fields() );
690   ft0A.setAttribute( QStringLiteral( "id_a" ), 31 );
691   layerA->startEditing();
692   layerA->addFeature( ft0A );
693   layerA->commitChanges();
694 
695   // add features for joined layers
696   QgsFeature ft0B( layerB->fields() );
697   ft0B.setAttribute( QStringLiteral( "id_b" ), 33 );
698   ft0B.setAttribute( QStringLiteral( "col0" ), 11 );
699   layerB->startEditing();
700   layerB->addFeature( ft0B );
701   layerB->commitChanges();
702 
703   QgsFeature ft0C( layerC->fields() );
704   ft0C.setAttribute( QStringLiteral( "id_c" ), 31 );
705   ft0C.setAttribute( QStringLiteral( "col0" ), 13 );
706   layerC->startEditing();
707   layerC->addFeature( ft0C );
708   layerC->commitChanges();
709 
710   // get committed feature from layerA
711   QgsFeature feature;
712   QString filter = QgsExpression::createFieldEqualityExpression( QStringLiteral( "id_a" ), 31 );
713 
714   QgsFeatureRequest request;
715   request.setFilterExpression( filter );
716   request.setLimit( 1 );
717   layerA->getFeatures( request ).nextFeature( ft0A );
718 
719   // start editing layers
720   layerA->startEditing();
721   layerB->startEditing();
722   layerC->startEditing();
723 
724   // build a form with feature A
725   QgsAttributeForm form( layerA );
726   form.setMode( QgsAttributeEditorContext::AddFeatureMode );
727   form.setFeature( ft0A );
728 
729   // count features
730   QCOMPARE( ( int )layerA->featureCount(), 1 );
731   QCOMPARE( ( int )layerB->featureCount(), 1 );
732   QCOMPARE( ( int )layerC->featureCount(), 1 );
733 
734   // add a new feature with null joined fields. Joined feature should not be
735   // added
736   form.changeAttribute( QStringLiteral( "id_a" ), QVariant( 32 ) );
737   form.changeAttribute( QStringLiteral( "layerB_col0" ), QVariant() );
738   form.changeAttribute( QStringLiteral( "layerC_col0" ), QVariant() );
739   form.save();
740 
741   // commit
742   layerA->commitChanges();
743   layerB->commitChanges();
744   layerC->commitChanges();
745 
746   // count features
747   QCOMPARE( ( int )layerA->featureCount(), 2 );
748   QCOMPARE( ( int )layerB->featureCount(), 1 );
749   QCOMPARE( ( int )layerC->featureCount(), 1 );
750 
751   // start editing layers
752   layerA->startEditing();
753   layerB->startEditing();
754   layerC->startEditing();
755 
756   // add a new feature with not null joined fields. Joined feature should be
757   // added
758   QgsAttributeForm form1( layerA );
759   form1.setMode( QgsAttributeEditorContext::AddFeatureMode );
760   form1.setFeature( ft0A );
761 
762   form1.changeAttribute( QStringLiteral( "id_a" ), QVariant( 34 ) );
763   form1.changeAttribute( QStringLiteral( "layerB_col0" ), QVariant( 3434 ) );
764   form1.changeAttribute( QStringLiteral( "layerC_col0" ), QVariant( 343434 ) );
765   form1.save();
766 
767   // commit
768   layerA->commitChanges();
769   layerB->commitChanges();
770   layerC->commitChanges();
771 
772   // count features
773   QCOMPARE( ( int )layerA->featureCount(), 3 );
774   QCOMPARE( ( int )layerB->featureCount(), 2 );
775   QCOMPARE( ( int )layerC->featureCount(), 1 );
776 
777   // check joined feature value
778   filter = QgsExpression::createFieldEqualityExpression( QStringLiteral( "id_a" ), 34 );
779 
780   request.setFilterExpression( filter );
781   request.setLimit( 1 );
782   layerA->getFeatures( request ).nextFeature( feature );
783 
784   QCOMPARE( feature.attribute( "layerB_col0" ), QVariant( 3434 ) );
785 
786   // start editing layers
787   layerA->startEditing();
788   layerB->startEditing();
789   layerC->startEditing();
790 
791   // create a target feature but update a joined feature. A new feature should
792   // be added in layerA and values in layerB should be updated
793   QgsAttributeForm form2( layerA );
794   form2.setMode( QgsAttributeEditorContext::AddFeatureMode );
795   form2.setFeature( ft0A );
796   form2.changeAttribute( QStringLiteral( "id_a" ), QVariant( 33 ) );
797   form2.changeAttribute( QStringLiteral( "layerB_col0" ), QVariant( 3333 ) );
798   form2.changeAttribute( QStringLiteral( "layerC_col0" ), QVariant( 323232 ) );
799   form2.save();
800 
801   // commit
802   layerA->commitChanges();
803   layerB->commitChanges();
804   layerC->commitChanges();
805 
806   // count features
807   QCOMPARE( ( int )layerA->featureCount(), 4 );
808   QCOMPARE( ( int )layerB->featureCount(), 2 );
809   QCOMPARE( ( int )layerC->featureCount(), 1 );
810 
811   // check joined feature value
812   filter = QgsExpression::createFieldEqualityExpression( QStringLiteral( "id_a" ), 33 );
813 
814   request.setFilterExpression( filter );
815   request.setLimit( 1 );
816   layerA->getFeatures( request ).nextFeature( feature );
817 
818   QCOMPARE( feature.attribute( "layerB_col0" ), QVariant( 3333 ) );
819 
820   // start editing layers
821   layerA->startEditing();
822   layerB->startEditing();
823   layerC->startEditing();
824 
825   // update feature which does not exist in joined layer but with null joined
826   // fields. A new feature should NOT be added in joined layer
827   QgsAttributeForm form3( layerA );
828   form3.setMode( QgsAttributeEditorContext::SingleEditMode );
829   form3.setFeature( ft0A );
830   form3.changeAttribute( QStringLiteral( "id_a" ), QVariant( 31 ) );
831   form3.changeAttribute( QStringLiteral( "layerB_col0" ), QVariant() );
832   form3.changeAttribute( QStringLiteral( "layerC_col0" ), QVariant() );
833   form3.save();
834 
835   // commit
836   layerA->commitChanges();
837   layerB->commitChanges();
838   layerC->commitChanges();
839 
840   // count features
841   QCOMPARE( ( int )layerA->featureCount(), 4 );
842   QCOMPARE( ( int )layerB->featureCount(), 2 );
843   QCOMPARE( ( int )layerC->featureCount(), 1 );
844 
845   // start editing layers
846   layerA->startEditing();
847   layerB->startEditing();
848   layerC->startEditing();
849 
850   // update feature which does not exist in joined layer with NOT null joined
851   // fields. A new feature should be added in joined layer
852   QgsAttributeForm form4( layerA );
853   form4.setMode( QgsAttributeEditorContext::SingleEditMode );
854   form4.setFeature( ft0A );
855   form4.changeAttribute( QStringLiteral( "id_a" ), QVariant( 31 ) );
856   form4.changeAttribute( QStringLiteral( "layerB_col0" ), QVariant( 1111 ) );
857   form4.changeAttribute( QStringLiteral( "layerC_col0" ), QVariant( 3131 ) );
858   form4.save();
859 
860   // commit
861   layerA->commitChanges();
862   layerB->commitChanges();
863   layerC->commitChanges();
864 
865   // count features
866   QCOMPARE( ( int )layerA->featureCount(), 4 );
867   QCOMPARE( ( int )layerB->featureCount(), 3 );
868   QCOMPARE( ( int )layerC->featureCount(), 1 );
869 
870   // check joined feature value
871   filter = QgsExpression::createFieldEqualityExpression( QStringLiteral( "id_a" ), 31 );
872 
873   request.setFilterExpression( filter );
874   request.setLimit( 1 );
875   layerA->getFeatures( request ).nextFeature( feature );
876 
877   QCOMPARE( feature.attribute( "layerB_col0" ), QVariant( 1111 ) );
878 
879   // clean
880   delete layerA;
881   delete layerB;
882   delete layerC;
883 }
884 
testFixAttributeForm()885 void TestQgsAttributeForm::testFixAttributeForm()
886 {
887   const QString def = QStringLiteral( "Point?field=id:integer&field=col1:integer" );
888   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "layer" ), QStringLiteral( "memory" ) );
889 
890   QVERIFY( layer );
891 
892   QgsFeature f( layer->fields() );
893   f.setAttribute( 0, 1 );
894   f.setAttribute( 1, 681 );
895 
896   QgsAttributeForm form( layer );
897 
898   form.setMode( QgsAttributeEditorContext::FixAttributeMode );
899   form.setFeature( f );
900 
901   QgsEditorWidgetWrapper *ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
902   QCOMPARE( ww->field().name(), QString( "col1" ) );
903   QCOMPARE( ww->value(), QVariant( 681 ) );
904 
905   // now change the value
906   ww->setValue( QVariant( 630 ) );
907 
908   // the value should be updated
909   QCOMPARE( ww->value(), QVariant( 630 ) );
910   // the feature is not saved yet, so contains the old value
911   QCOMPARE( form.feature().attribute( QStringLiteral( "col1" ) ), QVariant( 681 ) );
912   // now save the feature and enjoy its new value, but don't update the layer
913   QVERIFY( form.save() );
914   QCOMPARE( form.feature().attribute( QStringLiteral( "col1" ) ), QVariant( 630 ) );
915   QCOMPARE( ( int )layer->featureCount(), 0 );
916 
917   delete layer;
918 }
919 
testAttributeFormInterface()920 void TestQgsAttributeForm::testAttributeFormInterface()
921 {
922   // Issue https://github.com/qgis/QGIS/issues/29667
923   // we simulate a python code execution that would be triggered
924   // at form opening and that would modify the value of a widget.
925   // We want to check that emitted signal widgetValueChanged is
926   // correctly emitted with correct parameters
927 
928   // make a temporary vector layer
929   const QString def = QStringLiteral( "Point?field=col0:integer" );
930   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
931   layer->setEditorWidgetSetup( 0, QgsEditorWidgetSetup( QStringLiteral( "TextEdit" ), QVariantMap() ) );
932 
933   // add a feature to the vector layer
934   QgsFeature ft( layer->dataProvider()->fields(), 1 );
935   ft.setAttribute( QStringLiteral( "col0" ), 10 );
936 
937   class MyInterface : public QgsAttributeFormInterface
938   {
939     public:
940       MyInterface( QgsAttributeForm *form )
941         : QgsAttributeFormInterface( form ) {}
942 
943       virtual void featureChanged()
944       {
945         QgsAttributeForm *f = form();
946         QLineEdit *le = f->findChild<QLineEdit *>( "col0" );
947         le->setText( "100" );
948       }
949   };
950 
951   // build a form for this feature
952   QgsAttributeForm form( layer );
953   form.addInterface( new MyInterface( &form ) );
954 
955   bool set = false;
956   connect( &form, &QgsAttributeForm::widgetValueChanged, this,
957            [&set]( const QString & attribute, const QVariant & newValue, bool attributeChanged )
958   {
959 
960     // Check that our value set by the QgsAttributeFormInterface has correct parameters.
961     // attributeChanged has to be true because it won't be taken into account by others
962     // (QgsValueRelationWidgetWrapper for instance)
963     if ( attribute == "col0" && newValue.toInt() == 100 && attributeChanged )
964       set = true;
965   } );
966 
967   form.setFeature( ft );
968   QVERIFY( set );
969 }
970 
971 
testDefaultValueUpdate()972 void TestQgsAttributeForm::testDefaultValueUpdate()
973 {
974   // make a temporary layer to check through
975   const QString def = QStringLiteral( "Point?field=col0:integer&field=col1:integer&field=col2:integer&field=col3:integer" );
976   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
977 
978   //set defaultValueDefinitions
979   //col0 - no default value
980   //col1 - "col0"+1
981   //col2 - "col0"+"col1"
982   //col3 - "col2"
983 
984   // set constraints for each field
985   layer->setDefaultValueDefinition( 1, QgsDefaultValue( QStringLiteral( "\"col0\"+1" ) ) );
986   layer->setDefaultValueDefinition( 2, QgsDefaultValue( QStringLiteral( "\"col0\"+\"col1\"" ) ) );
987   layer->setDefaultValueDefinition( 3, QgsDefaultValue( QStringLiteral( "\"col2\"" ) ) );
988 
989   layer->startEditing();
990 
991   // build a form for this feature
992   QgsFeature ft( layer->dataProvider()->fields(), 1 );
993   ft.setAttribute( QStringLiteral( "col0" ), 0 );
994   QgsAttributeForm form( layer );
995   form.setMode( QgsAttributeEditorContext::AddFeatureMode );
996   form.setFeature( ft );
997 
998   // get wrappers for each widget
999   QgsEditorWidgetWrapper *ww0, *ww1, *ww2, *ww3;
1000   ww0 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
1001   ww1 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
1002   ww2 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
1003   ww3 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[3] );
1004 
1005   //set value in col0:
1006   ww0->setValue( 5 );
1007 
1008   //we expect
1009   //col0 - 5
1010   //col1 - 6
1011   //col2 - 11
1012   //col3 - 11
1013 
1014   QCOMPARE( ww0->value().toInt(), 5 );
1015   QCOMPARE( ww1->value().toInt(), 6 );
1016   QCOMPARE( ww2->value().toInt(), 11 );
1017   QCOMPARE( ww3->value().toInt(), 11 );
1018 
1019   //set value in col1:
1020   ww1->setValue( 10 );
1021 
1022   //we expect
1023   //col0 - 5
1024   //col1 - 10
1025   //col2 - 15
1026   //col3 - 15
1027 
1028   QCOMPARE( ww0->value().toInt(), 5 );
1029   QCOMPARE( ww1->value().toInt(), 10 );
1030   QCOMPARE( ww2->value().toInt(), 15 );
1031   QCOMPARE( ww3->value().toInt(), 15 );
1032 }
1033 
testDefaultValueUpdateRecursion()1034 void TestQgsAttributeForm::testDefaultValueUpdateRecursion()
1035 {
1036   // make a temporary layer to check through
1037   const QString def = QStringLiteral( "Point?field=col0:integer&field=col1:integer&field=col2:integer&field=col3:integer" );
1038   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
1039 
1040   //let's make a recursion
1041   //col0 - COALESCE( 0, "col3"+1)
1042   //col1 - COALESCE( 0, "col0"+1)
1043   //col2 - COALESCE( 0, "col1"+1)
1044   //col3 - COALESCE( 0, "col2"+1)
1045 
1046   // set constraints for each field
1047   layer->setDefaultValueDefinition( 0, QgsDefaultValue( QStringLiteral( "\"col3\"+1" ) ) );
1048   layer->setDefaultValueDefinition( 1, QgsDefaultValue( QStringLiteral( "\"col0\"+1" ) ) );
1049   layer->setDefaultValueDefinition( 2, QgsDefaultValue( QStringLiteral( "\"col1\"+1" ) ) );
1050   layer->setDefaultValueDefinition( 3, QgsDefaultValue( QStringLiteral( "\"col2\"+1" ) ) );
1051 
1052   layer->startEditing();
1053 
1054   // build a form for this feature
1055   QgsFeature ft( layer->dataProvider()->fields(), 1 );
1056   ft.setAttribute( QStringLiteral( "col0" ), 0 );
1057   QgsAttributeForm form( layer );
1058   form.setMode( QgsAttributeEditorContext::AddFeatureMode );
1059   form.setFeature( ft );
1060 
1061   // get wrappers for each widget
1062   QgsEditorWidgetWrapper *ww0, *ww1, *ww2, *ww3;
1063   ww0 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
1064   ww1 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
1065   ww2 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
1066   ww3 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[3] );
1067 
1068   //set value in col0:
1069   ww0->setValue( 20 );
1070 
1071   //we expect
1072   //col0 - 20
1073   //col1 - 21
1074   //col2 - 22
1075   //col3 - 23
1076 
1077   QCOMPARE( ww0->value().toInt(), 20 );
1078   QCOMPARE( ww1->value().toInt(), 21 );
1079   QCOMPARE( ww2->value().toInt(), 22 );
1080   QCOMPARE( ww3->value().toInt(), 23 );
1081 
1082   //set value in col2:
1083   ww2->setValue( 30 );
1084 
1085   //we expect
1086   //col0 - 32
1087   //col1 - 33
1088   //col2 - 30
1089   //col3 - 31
1090 
1091   QCOMPARE( ww0->value().toInt(), 32 );
1092   QCOMPARE( ww1->value().toInt(), 33 );
1093   QCOMPARE( ww2->value().toInt(), 30 );
1094   QCOMPARE( ww3->value().toInt(), 31 );
1095 
1096   //set value in col0 again:
1097   ww0->setValue( 40 );
1098 
1099   //we expect
1100   //col0 - 40
1101   //col1 - 41
1102   //col2 - 42
1103   //col3 - 43
1104 
1105   QCOMPARE( ww0->value().toInt(), 40 );
1106   QCOMPARE( ww1->value().toInt(), 41 );
1107   QCOMPARE( ww2->value().toInt(), 42 );
1108   QCOMPARE( ww3->value().toInt(), 43 );
1109 }
1110 
testSameFieldSync()1111 void TestQgsAttributeForm::testSameFieldSync()
1112 {
1113   // Check that widget synchronisation works when a form contains the same field several times
1114   // and there is no issues when editing
1115 
1116   // make a temporary vector layer
1117   const QString def = QStringLiteral( "Point?field=col0:integer" );
1118   QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );
1119   layer->setEditorWidgetSetup( 0, QgsEditorWidgetSetup( QStringLiteral( "TextEdit" ), QVariantMap() ) );
1120 
1121   // add a feature to the vector layer
1122   QgsFeature ft( layer->dataProvider()->fields(), 1 );
1123   ft.setAttribute( QStringLiteral( "col0" ), 10 );
1124 
1125   // add same field twice so they get synced
1126   QgsEditFormConfig editFormConfig = layer->editFormConfig();
1127   editFormConfig.clearTabs();
1128   editFormConfig.addTab( new QgsAttributeEditorField( "col0", 0, editFormConfig.invisibleRootContainer() ) );
1129   editFormConfig.addTab( new QgsAttributeEditorField( "col0", 0, editFormConfig.invisibleRootContainer() ) );
1130   editFormConfig.setLayout( QgsEditFormConfig::TabLayout );
1131   layer->setEditFormConfig( editFormConfig );
1132 
1133   layer->startEditing();
1134 
1135   // build a form for this feature
1136   QgsAttributeForm form( layer );
1137   form.setFeature( ft );
1138 
1139   QList<QLineEdit *> les = form.findChildren<QLineEdit *>( "col0" );
1140   QCOMPARE( les.count(), 2 );
1141 
1142   les[0]->setCursorPosition( 1 );
1143   QTest::keyClick( les[0], Qt::Key_2 );
1144   QTest::keyClick( les[0], Qt::Key_3 );
1145 
1146   QCOMPARE( les[0]->text(), QString( "1230" ) );
1147   QCOMPARE( les[0]->cursorPosition(), 3 );
1148   QCOMPARE( les[1]->text(), QString( "1230" ) );
1149   QCOMPARE( les[1]->cursorPosition(), 4 );
1150 }
1151 
testZeroDoubles()1152 void TestQgsAttributeForm::testZeroDoubles()
1153 {
1154   // See issue GH #34118
1155   const QString def = QStringLiteral( "Point?field=col0:double" );
1156   QgsVectorLayer layer { def, QStringLiteral( "test" ), QStringLiteral( "memory" ) };
1157   layer.setEditorWidgetSetup( 0, QgsEditorWidgetSetup( QStringLiteral( "TextEdit" ), QVariantMap() ) );
1158   QgsFeature ft( layer.dataProvider()->fields(), 1 );
1159   ft.setAttribute( QStringLiteral( "col0" ), 0.0 );
1160   QgsAttributeForm form( &layer );
1161   form.setFeature( ft );
1162   const QList<QLineEdit *> les = form.findChildren<QLineEdit *>( "col0" );
1163   QCOMPARE( les.count(), 1 );
1164   QCOMPARE( les.at( 0 )->text(), QStringLiteral( "0" ) );
1165 }
1166 
1167 QGSTEST_MAIN( TestQgsAttributeForm )
1168 #include "testqgsattributeform.moc"
1169