| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2017 The Qt Company Ltd. |
| 4 | ** Contact: http://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:LGPL3$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see http://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at http://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU Lesser General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation and appearing in the file LICENSE.LGPLv3 included in the |
| 21 | ** packaging of this file. Please review the following information to |
| 22 | ** ensure the GNU Lesser General Public License version 3 requirements |
| 23 | ** will be met: https://www.gnu.org/licenses/lgpl.html. |
| 24 | ** |
| 25 | ** GNU General Public License Usage |
| 26 | ** Alternatively, this file may be used under the terms of the GNU |
| 27 | ** General Public License version 2.0 or later as published by the Free |
| 28 | ** Software Foundation and appearing in the file LICENSE.GPL included in |
| 29 | ** the packaging of this file. Please review the following information to |
| 30 | ** ensure the GNU General Public License version 2.0 requirements will be |
| 31 | ** met: http://www.gnu.org/licenses/gpl-2.0.html. |
| 32 | ** |
| 33 | ** $QT_END_LICENSE$ |
| 34 | ** |
| 35 | ****************************************************************************/ |
| 36 | |
| 37 | #include <QtTest/qtest.h> |
| 38 | #include <QtCore/private/qhooks_p.h> |
| 39 | #include <QtCore/qregularexpression.h> |
| 40 | #include <QtQml/qqmlengine.h> |
| 41 | #include <QtQml/qqmlcomponent.h> |
| 42 | #include <QtQuick/qquickitem.h> |
| 43 | #include <QtQuick/qquickwindow.h> |
| 44 | #include <QtQuickControls2/qquickstyle.h> |
| 45 | #include <QtQuickTemplates2/private/qquickcontrol_p_p.h> |
| 46 | #include "../shared/visualtestutil.h" |
| 47 | |
| 48 | using namespace QQuickVisualTestUtil; |
| 49 | |
| 50 | struct ControlInfo |
| 51 | { |
| 52 | QString type; |
| 53 | QStringList delegates; |
| 54 | }; |
| 55 | |
| 56 | static const ControlInfo ControlInfos[] = { |
| 57 | { .type: "AbstractButton" , .delegates: QStringList() << "background" << "contentItem" << "indicator" }, |
| 58 | { .type: "ApplicationWindow" , .delegates: QStringList() << "background" }, |
| 59 | { .type: "BusyIndicator" , .delegates: QStringList() << "background" << "contentItem" }, |
| 60 | { .type: "Button" , .delegates: QStringList() << "background" << "contentItem" }, |
| 61 | { .type: "CheckBox" , .delegates: QStringList() << "contentItem" << "indicator" }, |
| 62 | { .type: "CheckDelegate" , .delegates: QStringList() << "background" << "contentItem" << "indicator" }, |
| 63 | { .type: "ComboBox" , .delegates: QStringList() << "background" << "contentItem" << "indicator" }, // popup not created until needed |
| 64 | { .type: "Container" , .delegates: QStringList() << "background" << "contentItem" }, |
| 65 | { .type: "Control" , .delegates: QStringList() << "background" << "contentItem" }, |
| 66 | { .type: "DelayButton" , .delegates: QStringList() << "background" << "contentItem" }, |
| 67 | { .type: "Dial" , .delegates: QStringList() << "background" << "handle" }, |
| 68 | { .type: "Dialog" , .delegates: QStringList() << "background" << "contentItem" }, |
| 69 | { .type: "DialogButtonBox" , .delegates: QStringList() << "background" << "contentItem" }, |
| 70 | { .type: "Drawer" , .delegates: QStringList() << "background" << "contentItem" }, |
| 71 | { .type: "Frame" , .delegates: QStringList() << "background" << "contentItem" }, |
| 72 | { .type: "GroupBox" , .delegates: QStringList() << "background" << "contentItem" << "label" }, |
| 73 | { .type: "ItemDelegate" , .delegates: QStringList() << "background" << "contentItem" }, |
| 74 | { .type: "Label" , .delegates: QStringList() << "background" }, |
| 75 | { .type: "Menu" , .delegates: QStringList() << "background" << "contentItem" }, |
| 76 | { .type: "MenuBar" , .delegates: QStringList() << "background" << "contentItem" }, |
| 77 | { .type: "MenuBarItem" , .delegates: QStringList() << "background" << "contentItem" }, |
| 78 | { .type: "MenuItem" , .delegates: QStringList() << "arrow" << "background" << "contentItem" << "indicator" }, |
| 79 | { .type: "MenuSeparator" , .delegates: QStringList() << "background" << "contentItem" }, |
| 80 | { .type: "Page" , .delegates: QStringList() << "background" << "contentItem" }, |
| 81 | { .type: "PageIndicator" , .delegates: QStringList() << "background" << "contentItem" }, |
| 82 | { .type: "Pane" , .delegates: QStringList() << "background" << "contentItem" }, |
| 83 | { .type: "Popup" , .delegates: QStringList() << "background" << "contentItem" }, |
| 84 | { .type: "ProgressBar" , .delegates: QStringList() << "background" << "contentItem" }, |
| 85 | { .type: "RadioButton" , .delegates: QStringList() << "contentItem" << "indicator" }, |
| 86 | { .type: "RadioDelegate" , .delegates: QStringList() << "background" << "contentItem" << "indicator" }, |
| 87 | { .type: "RangeSlider" , .delegates: QStringList() << "background" << "first.handle" << "second.handle" }, |
| 88 | { .type: "RoundButton" , .delegates: QStringList() << "background" << "contentItem" }, |
| 89 | { .type: "ScrollBar" , .delegates: QStringList() << "background" << "contentItem" }, |
| 90 | { .type: "ScrollIndicator" , .delegates: QStringList() << "background" << "contentItem" }, |
| 91 | { .type: "ScrollView" , .delegates: QStringList() << "background" }, |
| 92 | { .type: "Slider" , .delegates: QStringList() << "background" << "handle" }, |
| 93 | { .type: "SpinBox" , .delegates: QStringList() << "background" << "contentItem" << "up.indicator" << "down.indicator" }, |
| 94 | { .type: "StackView" , .delegates: QStringList() << "background" << "contentItem" }, |
| 95 | { .type: "SwipeDelegate" , .delegates: QStringList() << "background" << "contentItem" }, |
| 96 | { .type: "SwipeView" , .delegates: QStringList() << "background" << "contentItem" }, |
| 97 | { .type: "Switch" , .delegates: QStringList() << "contentItem" << "indicator" }, |
| 98 | { .type: "SwitchDelegate" , .delegates: QStringList() << "background" << "contentItem" << "indicator" }, |
| 99 | { .type: "TabBar" , .delegates: QStringList() << "background" << "contentItem" }, |
| 100 | { .type: "TabButton" , .delegates: QStringList() << "background" << "contentItem" }, |
| 101 | { .type: "TextField" , .delegates: QStringList() << "background" }, |
| 102 | { .type: "TextArea" , .delegates: QStringList() << "background" }, |
| 103 | { .type: "ToolBar" , .delegates: QStringList() << "background" << "contentItem" }, |
| 104 | { .type: "ToolButton" , .delegates: QStringList() << "background" << "contentItem" }, |
| 105 | { .type: "ToolSeparator" , .delegates: QStringList() << "background" << "contentItem" }, |
| 106 | { .type: "ToolTip" , .delegates: QStringList() << "background" << "contentItem" }, |
| 107 | { .type: "Tumbler" , .delegates: QStringList() << "background" << "contentItem" } |
| 108 | }; |
| 109 | |
| 110 | class tst_customization : public QQmlDataTest |
| 111 | { |
| 112 | Q_OBJECT |
| 113 | |
| 114 | private slots: |
| 115 | void initTestCase(); |
| 116 | void cleanupTestCase(); |
| 117 | |
| 118 | void init(); |
| 119 | void cleanup(); |
| 120 | |
| 121 | void creation_data(); |
| 122 | void creation(); |
| 123 | |
| 124 | void override_data(); |
| 125 | void override(); |
| 126 | |
| 127 | void comboPopup(); |
| 128 | |
| 129 | private: |
| 130 | void reset(); |
| 131 | void addHooks(); |
| 132 | void removeHooks(); |
| 133 | |
| 134 | QObject* createControl(const QString &type, const QString &qml, QString *error); |
| 135 | |
| 136 | QQmlEngine *engine = nullptr; |
| 137 | }; |
| 138 | |
| 139 | typedef QHash<QObject *, QString> QObjectNameHash; |
| 140 | Q_GLOBAL_STATIC(QObjectNameHash, qt_objectNames) |
| 141 | Q_GLOBAL_STATIC(QStringList, qt_createdQObjects) |
| 142 | Q_GLOBAL_STATIC(QStringList, qt_destroyedQObjects) |
| 143 | Q_GLOBAL_STATIC(QStringList, qt_destroyedParentQObjects) |
| 144 | static int qt_unparentedItemCount = 0; |
| 145 | |
| 146 | class ItemParentListener : public QQuickItem |
| 147 | { |
| 148 | Q_OBJECT |
| 149 | |
| 150 | public: |
| 151 | ItemParentListener() |
| 152 | { |
| 153 | m_slotIndex = metaObject()->indexOfSlot(slot: "onParentChanged()" ); |
| 154 | m_signalIndex = QMetaObjectPrivate::signalIndex(m: QMetaMethod::fromSignal(signal: &QQuickItem::parentChanged)); |
| 155 | } |
| 156 | |
| 157 | int signalIndex() const { return m_signalIndex; } |
| 158 | int slotIndex() const { return m_slotIndex; } |
| 159 | |
| 160 | public slots: |
| 161 | void onParentChanged() |
| 162 | { |
| 163 | const QQuickItem *item = qobject_cast<QQuickItem *>(object: sender()); |
| 164 | if (!item) |
| 165 | return; |
| 166 | |
| 167 | if (!item->parentItem()) |
| 168 | ++qt_unparentedItemCount; |
| 169 | } |
| 170 | |
| 171 | private: |
| 172 | int m_slotIndex; |
| 173 | int m_signalIndex; |
| 174 | }; |
| 175 | static ItemParentListener *qt_itemParentListener = nullptr; |
| 176 | |
| 177 | extern "C" Q_DECL_EXPORT void qt_addQObject(QObject *object) |
| 178 | { |
| 179 | // objectName is not set at construction time |
| 180 | QObject::connect(sender: object, signal: &QObject::objectNameChanged, slot: [object](const QString &objectName) { |
| 181 | QString oldObjectName = qt_objectNames()->value(key: object); |
| 182 | if (!oldObjectName.isEmpty()) |
| 183 | qt_createdQObjects()->removeOne(t: oldObjectName); |
| 184 | // Only track object names from our QML files, |
| 185 | // not e.g. contentItem object names (like "ApplicationWindow"). |
| 186 | if (objectName.contains(s: "-" )) { |
| 187 | qt_createdQObjects()->append(t: objectName); |
| 188 | qt_objectNames()->insert(key: object, value: objectName); |
| 189 | } |
| 190 | }); |
| 191 | |
| 192 | if (qt_itemParentListener) { |
| 193 | static const int signalIndex = qt_itemParentListener->signalIndex(); |
| 194 | static const int slotIndex = qt_itemParentListener->slotIndex(); |
| 195 | QMetaObject::connect(sender: object, signal_index: signalIndex, receiver: qt_itemParentListener, method_index: slotIndex); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | extern "C" Q_DECL_EXPORT void qt_removeQObject(QObject *object) |
| 200 | { |
| 201 | QString objectName = object->objectName(); |
| 202 | if (!objectName.isEmpty()) |
| 203 | qt_destroyedQObjects()->append(t: objectName); |
| 204 | qt_objectNames()->remove(key: object); |
| 205 | |
| 206 | QObject *parent = object->parent(); |
| 207 | if (parent) { |
| 208 | QString parentName = parent->objectName(); |
| 209 | if (!parentName.isEmpty()) |
| 210 | qt_destroyedParentQObjects()->append(t: parentName); |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | void tst_customization::initTestCase() |
| 215 | { |
| 216 | QQmlDataTest::initTestCase(); |
| 217 | |
| 218 | qt_itemParentListener = new ItemParentListener; |
| 219 | } |
| 220 | |
| 221 | void tst_customization::cleanupTestCase() |
| 222 | { |
| 223 | delete qt_itemParentListener; |
| 224 | qt_itemParentListener = nullptr; |
| 225 | } |
| 226 | |
| 227 | void tst_customization::init() |
| 228 | { |
| 229 | engine = new QQmlEngine(this); |
| 230 | |
| 231 | qtHookData[QHooks::AddQObject] = reinterpret_cast<quintptr>(&qt_addQObject); |
| 232 | qtHookData[QHooks::RemoveQObject] = reinterpret_cast<quintptr>(&qt_removeQObject); |
| 233 | } |
| 234 | |
| 235 | void tst_customization::cleanup() |
| 236 | { |
| 237 | qtHookData[QHooks::AddQObject] = 0; |
| 238 | qtHookData[QHooks::RemoveQObject] = 0; |
| 239 | |
| 240 | delete engine; |
| 241 | engine = nullptr; |
| 242 | |
| 243 | qmlClearTypeRegistrations(); |
| 244 | |
| 245 | reset(); |
| 246 | } |
| 247 | |
| 248 | void tst_customization::reset() |
| 249 | { |
| 250 | qt_unparentedItemCount = 0; |
| 251 | qt_createdQObjects()->clear(); |
| 252 | qt_destroyedQObjects()->clear(); |
| 253 | qt_destroyedParentQObjects()->clear(); |
| 254 | } |
| 255 | |
| 256 | QObject* tst_customization::createControl(const QString &name, const QString &qml, QString *error) |
| 257 | { |
| 258 | QQmlComponent component(engine); |
| 259 | component.setData("import QtQuick 2.10; import QtQuick.Window 2.2; import QtQuick.Controls 2.3; " + name.toUtf8() + " { " + qml.toUtf8() + " }" , baseUrl: QUrl()); |
| 260 | QObject *obj = component.create(); |
| 261 | if (!obj) |
| 262 | *error = component.errorString(); |
| 263 | return obj; |
| 264 | } |
| 265 | |
| 266 | void tst_customization::creation_data() |
| 267 | { |
| 268 | QTest::addColumn<QString>(name: "style" ); |
| 269 | QTest::addColumn<QString>(name: "type" ); |
| 270 | QTest::addColumn<QStringList>(name: "delegates" ); |
| 271 | |
| 272 | // the "empty" style does not contain any delegates |
| 273 | for (const ControlInfo &control : ControlInfos) |
| 274 | QTest::newRow(qPrintable("empty:" + control.type)) << "empty" << control.type << QStringList(); |
| 275 | |
| 276 | // the "incomplete" style is missing bindings to the delegates (must be created regardless) |
| 277 | for (const ControlInfo &control : ControlInfos) |
| 278 | QTest::newRow(qPrintable("incomplete:" + control.type)) << "incomplete" << control.type << control.delegates; |
| 279 | |
| 280 | // the "identified" style has IDs in the delegates (prevents deferred execution) |
| 281 | for (const ControlInfo &control : ControlInfos) |
| 282 | QTest::newRow(qPrintable("identified:" + control.type)) << "identified" << control.type << control.delegates; |
| 283 | |
| 284 | // the "simple" style simulates a proper style and contains bindings to/in delegates |
| 285 | for (const ControlInfo &control : ControlInfos) |
| 286 | QTest::newRow(qPrintable("simple:" + control.type)) << "simple" << control.type << control.delegates; |
| 287 | |
| 288 | // the "override" style overrides all delegates in the "simple" style |
| 289 | for (const ControlInfo &control : ControlInfos) |
| 290 | QTest::newRow(qPrintable("override:" + control.type)) << "override" << control.type << control.delegates; |
| 291 | } |
| 292 | |
| 293 | void tst_customization::creation() |
| 294 | { |
| 295 | QFETCH(QString, style); |
| 296 | QFETCH(QString, type); |
| 297 | QFETCH(QStringList, delegates); |
| 298 | |
| 299 | QQuickStyle::setStyle(testFile(fileName: "styles/" + style)); |
| 300 | |
| 301 | QString error; |
| 302 | QScopedPointer<QObject> control(createControl(name: type, qml: "" , error: &error)); |
| 303 | QVERIFY2(control, qPrintable(error)); |
| 304 | |
| 305 | QByteArray templateType = "QQuick" + type.toUtf8(); |
| 306 | QVERIFY2(control->inherits(templateType), qPrintable(type + " does not inherit " + templateType + " (" + control->metaObject()->className() + ")" )); |
| 307 | |
| 308 | // <control>-<style> |
| 309 | QString controlName = type.toLower() + "-" + style; |
| 310 | QCOMPARE(control->objectName(), controlName); |
| 311 | QVERIFY2(qt_createdQObjects()->removeOne(controlName), qPrintable(controlName + " was not created as expected" )); |
| 312 | |
| 313 | for (QString delegate : qAsConst(t&: delegates)) { |
| 314 | QStringList properties = delegate.split(sep: "." , behavior: Qt::SkipEmptyParts); |
| 315 | |
| 316 | // <control>-<delegate>-<style>(-<override>) |
| 317 | delegate.append(s: "-" + style); |
| 318 | delegate.prepend(s: type.toLower() + "-" ); |
| 319 | |
| 320 | QVERIFY2(qt_createdQObjects()->removeOne(delegate), qPrintable(delegate + " was not created as expected" )); |
| 321 | |
| 322 | // verify that the delegate instance has the expected object name |
| 323 | // in case of grouped properties, we must query the properties step by step |
| 324 | QObject *instance = control.data(); |
| 325 | while (!properties.isEmpty()) { |
| 326 | QString property = properties.takeFirst(); |
| 327 | instance = instance->property(name: property.toUtf8()).value<QObject *>(); |
| 328 | QVERIFY2(instance, qPrintable("property was null: " + property)); |
| 329 | } |
| 330 | QCOMPARE(instance->objectName(), delegate); |
| 331 | } |
| 332 | |
| 333 | QEXPECT_FAIL("identified:ComboBox" , "ComboBox::popup with an ID is created at construction time" , Continue); |
| 334 | |
| 335 | QVERIFY2(qt_createdQObjects()->isEmpty(), qPrintable("unexpectedly created: " + qt_createdQObjects->join(", " ))); |
| 336 | QVERIFY2(qt_destroyedQObjects()->isEmpty(), qPrintable("unexpectedly destroyed: " + qt_destroyedQObjects->join(", " ) + " were unexpectedly destroyed" )); |
| 337 | |
| 338 | QVERIFY2(qt_destroyedParentQObjects()->isEmpty(), qPrintable("delegates/children of: " + qt_destroyedParentQObjects->join(", " ) + " were unexpectedly destroyed" )); |
| 339 | } |
| 340 | |
| 341 | void tst_customization::override_data() |
| 342 | { |
| 343 | QTest::addColumn<QString>(name: "style" ); |
| 344 | QTest::addColumn<QString>(name: "type" ); |
| 345 | QTest::addColumn<QStringList>(name: "delegates" ); |
| 346 | QTest::addColumn<QString>(name: "nonDeferred" ); |
| 347 | QTest::addColumn<bool>(name: "identify" ); |
| 348 | |
| 349 | // NOTE: delegates with IDs prevent deferred execution |
| 350 | |
| 351 | // default delegates with IDs, override with custom delegates with no IDs |
| 352 | for (const ControlInfo &control : ControlInfos) |
| 353 | QTest::newRow(qPrintable("identified:" + control.type)) << "identified" << control.type << control.delegates << "identified" << false; |
| 354 | |
| 355 | // default delegates with no IDs, override with custom delegates with IDs |
| 356 | for (const ControlInfo &control : ControlInfos) |
| 357 | QTest::newRow(qPrintable("simple:" + control.type)) << "simple" << control.type << control.delegates << "" << true; |
| 358 | |
| 359 | // default delegates with IDs, override with custom delegates with IDs |
| 360 | for (const ControlInfo &control : ControlInfos) |
| 361 | QTest::newRow(qPrintable("overidentified:" + control.type)) << "identified" << control.type << control.delegates << "identified" << true; |
| 362 | |
| 363 | #ifndef Q_OS_MACOS // QTBUG-65671 |
| 364 | |
| 365 | // test that the built-in styles don't have undesired IDs in their delegates |
| 366 | const QStringList styles = QStringList() << "Default" << "Fusion" << "Material" << "Universal" ; // ### TODO: QQuickStyle::availableStyles(); |
| 367 | for (const QString &style : styles) { |
| 368 | for (const ControlInfo &control : ControlInfos) |
| 369 | QTest::newRow(qPrintable(style + ":" + control.type)) << style << control.type << control.delegates << "" << false; |
| 370 | } |
| 371 | |
| 372 | #endif |
| 373 | } |
| 374 | |
| 375 | void tst_customization::override() |
| 376 | { |
| 377 | QFETCH(QString, style); |
| 378 | QFETCH(QString, type); |
| 379 | QFETCH(QStringList, delegates); |
| 380 | QFETCH(QString, nonDeferred); |
| 381 | QFETCH(bool, identify); |
| 382 | |
| 383 | const QString testStyle = testFile(fileName: "styles/" + style); |
| 384 | if (QDir(testStyle).exists()) |
| 385 | QQuickStyle::setStyle(testStyle); |
| 386 | else |
| 387 | QQuickStyle::setStyle(style); |
| 388 | |
| 389 | QString qml; |
| 390 | qml += QString("objectName: '%1-%2-override'; " ).arg(a: type.toLower()).arg(a: style); |
| 391 | for (const QString &delegate : delegates) { |
| 392 | QString id = identify ? QString("id: %1;" ).arg(a: delegate) : QString(); |
| 393 | qml += QString("%1: Item { %2 objectName: '%3-%1-%4-override' } " ).arg(a: delegate).arg(a: id.replace(before: "." , after: "" )).arg(a: type.toLower()).arg(a: style); |
| 394 | } |
| 395 | |
| 396 | QString error; |
| 397 | QScopedPointer<QObject> control(createControl(name: type, qml, error: &error)); |
| 398 | QVERIFY2(control, qPrintable(error)); |
| 399 | |
| 400 | // If there are no intentional IDs in the default delegates nor in the overridden custom |
| 401 | // delegates, no item should get un-parented during the creation process. An item being |
| 402 | // unparented means that a delegate got destroyed, so there must be an internal ID in one |
| 403 | // of the delegates in the tested style. |
| 404 | if (!identify && nonDeferred.isEmpty()) { |
| 405 | QEXPECT_FAIL("Universal:ApplicationWindow" , "ApplicationWindow.qml contains an intentionally unparented FocusRectangle" , Continue); |
| 406 | QCOMPARE(qt_unparentedItemCount, 0); |
| 407 | } |
| 408 | |
| 409 | // <control>-<style>-override |
| 410 | QString controlName = type.toLower() + "-" + style + "-override" ; |
| 411 | QCOMPARE(control->objectName(), controlName); |
| 412 | QVERIFY2(qt_createdQObjects()->removeOne(controlName), qPrintable(controlName + " was not created as expected" )); |
| 413 | |
| 414 | for (QString delegate : qAsConst(t&: delegates)) { |
| 415 | QStringList properties = delegate.split(sep: "." , behavior: Qt::SkipEmptyParts); |
| 416 | |
| 417 | // <control>-<delegate>-<style>(-override) |
| 418 | delegate.append(s: "-" + style); |
| 419 | delegate.prepend(s: type.toLower() + "-" ); |
| 420 | |
| 421 | if (!nonDeferred.isEmpty()) |
| 422 | QVERIFY2(qt_createdQObjects()->removeOne(delegate), qPrintable(delegate + " was not created as expected" )); |
| 423 | |
| 424 | delegate.append(s: "-override" ); |
| 425 | QVERIFY2(qt_createdQObjects()->removeOne(delegate), qPrintable(delegate + " was not created as expected" )); |
| 426 | |
| 427 | // verify that the delegate instance has the expected object name |
| 428 | // in case of grouped properties, we must query the properties step by step |
| 429 | QObject *instance = control.data(); |
| 430 | while (!properties.isEmpty()) { |
| 431 | QString property = properties.takeFirst(); |
| 432 | instance = instance->property(name: property.toUtf8()).value<QObject *>(); |
| 433 | QVERIFY2(instance, qPrintable("property was null: " + property)); |
| 434 | } |
| 435 | QCOMPARE(instance->objectName(), delegate); |
| 436 | } |
| 437 | |
| 438 | QEXPECT_FAIL("identified:ComboBox" , "ComboBox::popup with an ID is created at construction time" , Continue); |
| 439 | QEXPECT_FAIL("overidentified:ComboBox" , "ComboBox::popup with an ID is created at construction time" , Continue); |
| 440 | QVERIFY2(qt_createdQObjects()->isEmpty(), qPrintable("unexpectedly created: " + qt_createdQObjects->join(", " ))); |
| 441 | |
| 442 | if (!nonDeferred.isEmpty()) { |
| 443 | // There were items for which deferred execution was not possible. |
| 444 | for (QString delegateName : qAsConst(t&: delegates)) { |
| 445 | if (!delegateName.contains(s: "-" )) |
| 446 | delegateName.append(s: "-" + nonDeferred); |
| 447 | delegateName.prepend(s: type.toLower() + "-" ); |
| 448 | |
| 449 | const int delegateIndex = qt_destroyedQObjects()->indexOf(t: delegateName); |
| 450 | QVERIFY2(delegateIndex == -1, qPrintable(delegateName + " was unexpectedly destroyed" )); |
| 451 | |
| 452 | const auto controlChildren = control->children(); |
| 453 | const auto childIt = std::find_if(first: controlChildren.constBegin(), last: controlChildren.constEnd(), pred: [delegateName](const QObject *child) { |
| 454 | return child->objectName() == delegateName; |
| 455 | }); |
| 456 | // We test other delegates (like the background) here, so make sure we don't end up with XPASSes by using the wrong delegate. |
| 457 | if (delegateName.contains(s: QLatin1String("handle" ))) { |
| 458 | QEXPECT_FAIL("identified:RangeSlider" , "For some reason, items that are belong to grouped properties fail here" , Abort); |
| 459 | QEXPECT_FAIL("overidentified:RangeSlider" , "For some reason, items that are belong to grouped properties fail here" , Abort); |
| 460 | } |
| 461 | if (delegateName.contains(s: QLatin1String("indicator" ))) { |
| 462 | QEXPECT_FAIL("identified:SpinBox" , "For some reason, items that are belong to grouped properties fail here" , Abort); |
| 463 | QEXPECT_FAIL("overidentified:SpinBox" , "For some reason, items that are belong to grouped properties fail here" , Abort); |
| 464 | } |
| 465 | QVERIFY2(childIt != controlChildren.constEnd(), qPrintable(QString::fromLatin1( |
| 466 | "Expected delegate \"%1\" to still be a QObject child of \"%2\"" ).arg(delegateName).arg(controlName))); |
| 467 | |
| 468 | const auto *delegate = qobject_cast<QQuickItem*>(object: *childIt); |
| 469 | // Ensure that the item is hidden, etc. |
| 470 | QVERIFY(delegate); |
| 471 | QCOMPARE(delegate->isVisible(), false); |
| 472 | QCOMPARE(delegate->parentItem(), nullptr); |
| 473 | } |
| 474 | } |
| 475 | |
| 476 | QVERIFY2(qt_destroyedQObjects()->isEmpty(), qPrintable("unexpectedly destroyed: " + qt_destroyedQObjects->join(", " ))); |
| 477 | } |
| 478 | |
| 479 | void tst_customization::() |
| 480 | { |
| 481 | QQuickStyle::setStyle(testFile(fileName: "styles/simple" )); |
| 482 | |
| 483 | { |
| 484 | // test that ComboBox::popup is created when accessed |
| 485 | QQmlComponent component(engine); |
| 486 | component.setData("import QtQuick.Controls 2.2; ComboBox { }" , baseUrl: QUrl()); |
| 487 | QScopedPointer<QQuickItem> comboBox(qobject_cast<QQuickItem *>(object: component.create())); |
| 488 | QVERIFY(comboBox); |
| 489 | |
| 490 | QVERIFY(!qt_createdQObjects()->contains("combobox-popup-simple" )); |
| 491 | |
| 492 | QObject * = comboBox->property(name: "popup" ).value<QObject *>(); |
| 493 | QVERIFY(popup); |
| 494 | QVERIFY(qt_createdQObjects()->contains("combobox-popup-simple" )); |
| 495 | } |
| 496 | |
| 497 | reset(); |
| 498 | |
| 499 | { |
| 500 | // test that ComboBox::popup is created when it becomes visible |
| 501 | QQuickWindow window; |
| 502 | window.resize(w: 300, h: 300); |
| 503 | window.show(); |
| 504 | window.requestActivate(); |
| 505 | QVERIFY(QTest::qWaitForWindowActive(&window)); |
| 506 | |
| 507 | QQmlComponent component(engine); |
| 508 | component.setData("import QtQuick.Controls 2.2; ComboBox { }" , baseUrl: QUrl()); |
| 509 | QScopedPointer<QQuickItem> comboBox(qobject_cast<QQuickItem *>(object: component.create())); |
| 510 | QVERIFY(comboBox); |
| 511 | |
| 512 | comboBox->setParentItem(window.contentItem()); |
| 513 | QVERIFY(!qt_createdQObjects()->contains("combobox-popup-simple" )); |
| 514 | |
| 515 | QTest::mouseClick(window: &window, button: Qt::LeftButton, stateKey: Qt::NoModifier, pos: QPoint(1, 1)); |
| 516 | QVERIFY(qt_createdQObjects()->contains("combobox-popup-simple" )); |
| 517 | } |
| 518 | |
| 519 | reset(); |
| 520 | |
| 521 | { |
| 522 | // test that ComboBox::popup is completed upon component completion (if appropriate) |
| 523 | QQmlComponent component(engine); |
| 524 | component.setData("import QtQuick 2.9; import QtQuick.Controls 2.2; ComboBox { id: control; contentItem: Item { visible: !control.popup.visible } popup: Popup { property bool wasCompleted: false; Component.onCompleted: wasCompleted = true } }" , baseUrl: QUrl()); |
| 525 | QScopedPointer<QQuickItem> comboBox(qobject_cast<QQuickItem *>(object: component.create())); |
| 526 | QVERIFY(comboBox); |
| 527 | |
| 528 | QObject * = comboBox->property(name: "popup" ).value<QObject *>(); |
| 529 | QVERIFY(popup); |
| 530 | QCOMPARE(popup->property("wasCompleted" ), QVariant(true)); |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | QTEST_MAIN(tst_customization) |
| 535 | |
| 536 | #include "tst_customization.moc" |
| 537 | |