aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto/quickcontrols2/customization/tst_customization.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/quickcontrols2/customization/tst_customization.cpp')
-rw-r--r--tests/auto/quickcontrols2/customization/tst_customization.cpp534
1 files changed, 534 insertions, 0 deletions
diff --git a/tests/auto/quickcontrols2/customization/tst_customization.cpp b/tests/auto/quickcontrols2/customization/tst_customization.cpp
new file mode 100644
index 0000000000..02c7597863
--- /dev/null
+++ b/tests/auto/quickcontrols2/customization/tst_customization.cpp
@@ -0,0 +1,534 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/qtest.h>
+#include <QtCore/private/qhooks_p.h>
+#include <QtCore/qregularexpression.h>
+#include <QtQml/qqmlengine.h>
+#include <QtQml/qqmlcomponent.h>
+#include <QtQuick/qquickitem.h>
+#include <QtQuick/qquickwindow.h>
+#include <QtQuickControls2/qquickstyle.h>
+#include <QtQuickControls2/private/qquickstyle_p.h>
+#include <QtQuickTemplates2/private/qquickcontrol_p_p.h>
+#include "../shared/visualtestutil.h"
+
+using namespace QQuickVisualTestUtil;
+
+struct ControlInfo
+{
+ QString type;
+ QStringList delegates;
+};
+
+static const ControlInfo ControlInfos[] = {
+ { "AbstractButton", QStringList() << "background" << "contentItem" << "indicator" },
+ { "ApplicationWindow", QStringList() << "background" },
+ { "BusyIndicator", QStringList() << "background" << "contentItem" },
+ { "Button", QStringList() << "background" << "contentItem" },
+ { "CheckBox", QStringList() << "contentItem" << "indicator" },
+ { "CheckDelegate", QStringList() << "background" << "contentItem" << "indicator" },
+ { "ComboBox", QStringList() << "background" << "contentItem" << "indicator" }, // popup not created until needed
+ { "Container", QStringList() << "background" << "contentItem" },
+ { "Control", QStringList() << "background" << "contentItem" },
+ { "DelayButton", QStringList() << "background" << "contentItem" },
+ { "Dial", QStringList() << "background" << "handle" },
+ { "Dialog", QStringList() << "background" << "contentItem" },
+ { "DialogButtonBox", QStringList() << "background" << "contentItem" },
+ { "Drawer", QStringList() << "background" << "contentItem" },
+ { "Frame", QStringList() << "background" << "contentItem" },
+ { "GroupBox", QStringList() << "background" << "contentItem" << "label" },
+ { "ItemDelegate", QStringList() << "background" << "contentItem" },
+ { "Label", QStringList() << "background" },
+ { "Menu", QStringList() << "background" << "contentItem" },
+ { "MenuBar", QStringList() << "background" << "contentItem" },
+ { "MenuBarItem", QStringList() << "background" << "contentItem" },
+ { "MenuItem", QStringList() << "arrow" << "background" << "contentItem" << "indicator" },
+ { "MenuSeparator", QStringList() << "background" << "contentItem" },
+ { "Page", QStringList() << "background" << "contentItem" },
+ { "PageIndicator", QStringList() << "background" << "contentItem" },
+ { "Pane", QStringList() << "background" << "contentItem" },
+ { "Popup", QStringList() << "background" << "contentItem" },
+ { "ProgressBar", QStringList() << "background" << "contentItem" },
+ { "RadioButton", QStringList() << "contentItem" << "indicator" },
+ { "RadioDelegate", QStringList() << "background" << "contentItem" << "indicator" },
+ { "RangeSlider", QStringList() << "background" << "first.handle" << "second.handle" },
+ { "RoundButton", QStringList() << "background" << "contentItem" },
+ { "ScrollBar", QStringList() << "background" << "contentItem" },
+ { "ScrollIndicator", QStringList() << "background" << "contentItem" },
+ { "ScrollView", QStringList() << "background" },
+ { "Slider", QStringList() << "background" << "handle" },
+ { "SpinBox", QStringList() << "background" << "contentItem" << "up.indicator" << "down.indicator" },
+ { "StackView", QStringList() << "background" << "contentItem" },
+ { "SwipeDelegate", QStringList() << "background" << "contentItem" },
+ { "SwipeView", QStringList() << "background" << "contentItem" },
+ { "Switch", QStringList() << "contentItem" << "indicator" },
+ { "SwitchDelegate", QStringList() << "background" << "contentItem" << "indicator" },
+ { "TabBar", QStringList() << "background" << "contentItem" },
+ { "TabButton", QStringList() << "background" << "contentItem" },
+ { "TextField", QStringList() << "background" },
+ { "TextArea", QStringList() << "background" },
+ { "ToolBar", QStringList() << "background" << "contentItem" },
+ { "ToolButton", QStringList() << "background" << "contentItem" },
+ { "ToolSeparator", QStringList() << "background" << "contentItem" },
+ { "ToolTip", QStringList() << "background" << "contentItem" },
+ { "Tumbler", QStringList() << "background" << "contentItem" }
+};
+
+class tst_customization : public QQmlDataTest
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase() override;
+ void cleanupTestCase();
+
+ void init();
+ void cleanup();
+
+ void creation_data();
+ void creation();
+
+ void override_data();
+ void override();
+
+ void comboPopup();
+
+private:
+ void reset();
+ void addHooks();
+ void removeHooks();
+
+ QObject* createControl(const QString &type, const QString &qml, QString *error);
+
+ QQmlEngine *engine = nullptr;
+};
+
+typedef QHash<QObject *, QString> QObjectNameHash;
+Q_GLOBAL_STATIC(QObjectNameHash, qt_objectNames)
+Q_GLOBAL_STATIC(QStringList, qt_createdQObjects)
+Q_GLOBAL_STATIC(QStringList, qt_destroyedQObjects)
+Q_GLOBAL_STATIC(QStringList, qt_destroyedParentQObjects)
+static int qt_unparentedItemCount = 0;
+
+class ItemParentListener : public QQuickItem
+{
+ Q_OBJECT
+
+public:
+ ItemParentListener()
+ {
+ m_slotIndex = metaObject()->indexOfSlot("onParentChanged()");
+ m_signalIndex = QMetaObjectPrivate::signalIndex(QMetaMethod::fromSignal(&QQuickItem::parentChanged));
+ }
+
+ int signalIndex() const { return m_signalIndex; }
+ int slotIndex() const { return m_slotIndex; }
+
+public slots:
+ void onParentChanged()
+ {
+ const QQuickItem *item = qobject_cast<QQuickItem *>(sender());
+ if (!item)
+ return;
+
+ if (!item->parentItem())
+ ++qt_unparentedItemCount;
+ }
+
+private:
+ int m_slotIndex;
+ int m_signalIndex;
+};
+static ItemParentListener *qt_itemParentListener = nullptr;
+
+extern "C" Q_DECL_EXPORT void qt_addQObject(QObject *object)
+{
+ // objectName is not set at construction time
+ QObject::connect(object, &QObject::objectNameChanged, [object](const QString &objectName) {
+ QString oldObjectName = qt_objectNames()->value(object);
+ if (!oldObjectName.isEmpty())
+ qt_createdQObjects()->removeOne(oldObjectName);
+ // Only track object names from our QML files,
+ // not e.g. contentItem object names (like "ApplicationWindow").
+ if (objectName.contains("-")) {
+ qt_createdQObjects()->append(objectName);
+ qt_objectNames()->insert(object, objectName);
+ }
+ });
+
+ if (qt_itemParentListener) {
+ static const int signalIndex = qt_itemParentListener->signalIndex();
+ static const int slotIndex = qt_itemParentListener->slotIndex();
+ QMetaObject::connect(object, signalIndex, qt_itemParentListener, slotIndex);
+ }
+}
+
+extern "C" Q_DECL_EXPORT void qt_removeQObject(QObject *object)
+{
+ QString objectName = object->objectName();
+ if (!objectName.isEmpty())
+ qt_destroyedQObjects()->append(objectName);
+ qt_objectNames()->remove(object);
+
+ QObject *parent = object->parent();
+ if (parent) {
+ QString parentName = parent->objectName();
+ if (!parentName.isEmpty())
+ qt_destroyedParentQObjects()->append(parentName);
+ }
+}
+
+void tst_customization::initTestCase()
+{
+ QQmlDataTest::initTestCase();
+
+ qt_itemParentListener = new ItemParentListener;
+}
+
+void tst_customization::cleanupTestCase()
+{
+ delete qt_itemParentListener;
+ qt_itemParentListener = nullptr;
+}
+
+void tst_customization::init()
+{
+ engine = new QQmlEngine(this);
+ engine->addImportPath(testFile("styles"));
+
+ qtHookData[QHooks::AddQObject] = reinterpret_cast<quintptr>(&qt_addQObject);
+ qtHookData[QHooks::RemoveQObject] = reinterpret_cast<quintptr>(&qt_removeQObject);
+}
+
+void tst_customization::cleanup()
+{
+ qtHookData[QHooks::AddQObject] = 0;
+ qtHookData[QHooks::RemoveQObject] = 0;
+
+ delete engine;
+ engine = nullptr;
+
+ qmlClearTypeRegistrations();
+
+ reset();
+}
+
+void tst_customization::reset()
+{
+ qt_unparentedItemCount = 0;
+ qt_createdQObjects()->clear();
+ qt_destroyedQObjects()->clear();
+ qt_destroyedParentQObjects()->clear();
+}
+
+QObject* tst_customization::createControl(const QString &name, const QString &qml, QString *error)
+{
+ QQmlComponent component(engine);
+ component.setData("import QtQuick; import QtQuick.Window; import QtQuick.Controls; " + name.toUtf8() + " { " + qml.toUtf8() + " }", QUrl());
+ QObject *obj = component.create();
+ if (!obj)
+ *error = component.errorString();
+ return obj;
+}
+
+void tst_customization::creation_data()
+{
+ QTest::addColumn<QString>("style");
+ QTest::addColumn<QString>("type");
+ QTest::addColumn<QStringList>("delegates");
+
+ // the "empty" style does not contain any delegates
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("empty:" + control.type)) << "empty" << control.type << QStringList();
+
+ // the "incomplete" style is missing bindings to the delegates (must be created regardless)
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("incomplete:" + control.type)) << "incomplete" << control.type << control.delegates;
+
+ // the "identified" style has IDs in the delegates (prevents deferred execution)
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("identified:" + control.type)) << "identified" << control.type << control.delegates;
+
+ // the "simple" style simulates a proper style and contains bindings to/in delegates
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("simple:" + control.type)) << "simple" << control.type << control.delegates;
+
+ // the "override" style overrides all delegates in the "simple" style
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("override:" + control.type)) << "override" << control.type << control.delegates;
+}
+
+void tst_customization::creation()
+{
+ QFETCH(QString, style);
+ QFETCH(QString, type);
+ QFETCH(QStringList, delegates);
+
+ QQuickStyle::setStyle(style);
+
+ QString error;
+ QScopedPointer<QObject> control(createControl(type, "", &error));
+ QVERIFY2(control, qPrintable(error));
+
+ QByteArray templateType = "QQuick" + type.toUtf8();
+ QVERIFY2(control->inherits(templateType), qPrintable(type + " does not inherit " + templateType + " (" + control->metaObject()->className() + ")"));
+
+ // <control>-<style>
+ QString controlName = type.toLower() + "-" + style;
+ QCOMPARE(control->objectName(), controlName);
+ QVERIFY2(qt_createdQObjects()->removeOne(controlName), qPrintable(controlName + " was not created as expected"));
+
+ for (QString delegate : qAsConst(delegates)) {
+ QStringList properties = delegate.split(".", Qt::SkipEmptyParts);
+
+ // <control>-<delegate>-<style>(-<override>)
+ delegate.append("-" + style);
+ delegate.prepend(type.toLower() + "-");
+
+ QVERIFY2(qt_createdQObjects()->removeOne(delegate), qPrintable(delegate + " was not created as expected"));
+
+ // verify that the delegate instance has the expected object name
+ // in case of grouped properties, we must query the properties step by step
+ QObject *instance = control.data();
+ while (!properties.isEmpty()) {
+ QString property = properties.takeFirst();
+ instance = instance->property(property.toUtf8()).value<QObject *>();
+ QVERIFY2(instance, qPrintable("property was null: " + property));
+ }
+ QCOMPARE(instance->objectName(), delegate);
+ }
+
+ QEXPECT_FAIL("identified:ComboBox", "ComboBox::popup with an ID is created at construction time", Continue);
+
+ QVERIFY2(qt_createdQObjects()->isEmpty(), qPrintable("unexpectedly created: " + qt_createdQObjects->join(", ")));
+ QVERIFY2(qt_destroyedQObjects()->isEmpty(), qPrintable("unexpectedly destroyed: " + qt_destroyedQObjects->join(", ") + " were unexpectedly destroyed"));
+
+ QVERIFY2(qt_destroyedParentQObjects()->isEmpty(), qPrintable("delegates/children of: " + qt_destroyedParentQObjects->join(", ") + " were unexpectedly destroyed"));
+}
+
+void tst_customization::override_data()
+{
+ QTest::addColumn<QString>("style");
+ QTest::addColumn<QString>("type");
+ QTest::addColumn<QStringList>("delegates");
+ QTest::addColumn<QString>("nonDeferred");
+ QTest::addColumn<bool>("identify");
+
+ // NOTE: delegates with IDs prevent deferred execution
+
+ // default delegates with IDs, override with custom delegates with no IDs
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("identified:" + control.type)) << "identified" << control.type << control.delegates << "identified" << false;
+
+ // default delegates with no IDs, override with custom delegates with IDs
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("simple:" + control.type)) << "simple" << control.type << control.delegates << "" << true;
+
+ // default delegates with IDs, override with custom delegates with IDs
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable("overidentified:" + control.type)) << "identified" << control.type << control.delegates << "identified" << true;
+
+#ifndef Q_OS_MACOS // QTBUG-65671
+
+ // test that the built-in styles don't have undesired IDs in their delegates
+ const QStringList styles = QQuickStylePrivate::builtInStyles();
+ for (const QString &style : styles) {
+ for (const ControlInfo &control : ControlInfos)
+ QTest::newRow(qPrintable(style + ":" + control.type)) << style << control.type << control.delegates << "" << false;
+ }
+
+#endif
+}
+
+void tst_customization::override()
+{
+ QFETCH(QString, style);
+ QFETCH(QString, type);
+ QFETCH(QStringList, delegates);
+ QFETCH(QString, nonDeferred);
+ QFETCH(bool, identify);
+
+ QQuickStyle::setStyle(style);
+
+ QString qml;
+ qml += QString("objectName: '%1-%2-override'; ").arg(type.toLower()).arg(style);
+ for (const QString &delegate : delegates) {
+ QString id = identify ? QString("id: %1;").arg(delegate) : QString();
+ qml += QString("%1: Item { %2 objectName: '%3-%1-%4-override' } ").arg(delegate).arg(id.replace(".", "")).arg(type.toLower()).arg(style);
+ }
+
+ QString error;
+ QScopedPointer<QObject> control(createControl(type, qml, &error));
+ QVERIFY2(control, qPrintable(error));
+
+ // If there are no intentional IDs in the default delegates nor in the overridden custom
+ // delegates, no item should get un-parented during the creation process. An item being
+ // unparented means that a delegate got destroyed, so there must be an internal ID in one
+ // of the delegates in the tested style.
+ if (!identify && nonDeferred.isEmpty()) {
+ QEXPECT_FAIL("Universal:ApplicationWindow", "ApplicationWindow.qml contains an intentionally unparented FocusRectangle", Continue);
+ QCOMPARE(qt_unparentedItemCount, 0);
+ }
+
+ // <control>-<style>-override
+ QString controlName = type.toLower() + "-" + style + "-override";
+ QCOMPARE(control->objectName(), controlName);
+ QVERIFY2(qt_createdQObjects()->removeOne(controlName), qPrintable(controlName + " was not created as expected"));
+
+ for (QString delegate : qAsConst(delegates)) {
+ QStringList properties = delegate.split(".", Qt::SkipEmptyParts);
+
+ // <control>-<delegate>-<style>(-override)
+ delegate.append("-" + style);
+ delegate.prepend(type.toLower() + "-");
+
+ if (!nonDeferred.isEmpty())
+ QVERIFY2(qt_createdQObjects()->removeOne(delegate), qPrintable(delegate + " was not created as expected"));
+
+ delegate.append("-override");
+ QVERIFY2(qt_createdQObjects()->removeOne(delegate), qPrintable(delegate + " was not created as expected"));
+
+ // verify that the delegate instance has the expected object name
+ // in case of grouped properties, we must query the properties step by step
+ QObject *instance = control.data();
+ while (!properties.isEmpty()) {
+ QString property = properties.takeFirst();
+ instance = instance->property(property.toUtf8()).value<QObject *>();
+ QVERIFY2(instance, qPrintable("property was null: " + property));
+ }
+ QCOMPARE(instance->objectName(), delegate);
+ }
+
+ QEXPECT_FAIL("identified:ComboBox", "ComboBox::popup with an ID is created at construction time", Continue);
+ QEXPECT_FAIL("overidentified:ComboBox", "ComboBox::popup with an ID is created at construction time", Continue);
+ QVERIFY2(qt_createdQObjects()->isEmpty(), qPrintable("unexpectedly created: " + qt_createdQObjects->join(", ")));
+
+ if (!nonDeferred.isEmpty()) {
+ // There were items for which deferred execution was not possible.
+ for (QString delegateName : qAsConst(delegates)) {
+ if (!delegateName.contains("-"))
+ delegateName.append("-" + nonDeferred);
+ delegateName.prepend(type.toLower() + "-");
+
+ const int delegateIndex = qt_destroyedQObjects()->indexOf(delegateName);
+ QVERIFY2(delegateIndex == -1, qPrintable(delegateName + " was unexpectedly destroyed"));
+
+ const auto controlChildren = control->children();
+ const auto childIt = std::find_if(controlChildren.constBegin(), controlChildren.constEnd(), [delegateName](const QObject *child) {
+ return child->objectName() == delegateName;
+ });
+ // We test other delegates (like the background) here, so make sure we don't end up with XPASSes by using the wrong delegate.
+ if (delegateName.contains(QLatin1String("handle"))) {
+ QEXPECT_FAIL("identified:RangeSlider", "For some reason, items that are belong to grouped properties fail here", Abort);
+ QEXPECT_FAIL("overidentified:RangeSlider", "For some reason, items that are belong to grouped properties fail here", Abort);
+ }
+ if (delegateName.contains(QLatin1String("indicator"))) {
+ QEXPECT_FAIL("identified:SpinBox", "For some reason, items that are belong to grouped properties fail here", Abort);
+ QEXPECT_FAIL("overidentified:SpinBox", "For some reason, items that are belong to grouped properties fail here", Abort);
+ }
+ QVERIFY2(childIt != controlChildren.constEnd(), qPrintable(QString::fromLatin1(
+ "Expected delegate \"%1\" to still be a QObject child of \"%2\"").arg(delegateName).arg(controlName)));
+
+ const auto *delegate = qobject_cast<QQuickItem*>(*childIt);
+ // Ensure that the item is hidden, etc.
+ QVERIFY(delegate);
+ QCOMPARE(delegate->isVisible(), false);
+ QCOMPARE(delegate->parentItem(), nullptr);
+ }
+ }
+
+ QVERIFY2(qt_destroyedQObjects()->isEmpty(), qPrintable("unexpectedly destroyed: " + qt_destroyedQObjects->join(", ")));
+}
+
+void tst_customization::comboPopup()
+{
+ QQuickStyle::setStyle("simple");
+
+ {
+ // test that ComboBox::popup is created when accessed
+ QQmlComponent component(engine);
+ component.setData("import QtQuick.Controls; ComboBox { }", QUrl());
+ QScopedPointer<QQuickItem> comboBox(qobject_cast<QQuickItem *>(component.create()));
+ QVERIFY(comboBox);
+
+ QVERIFY(!qt_createdQObjects()->contains("combobox-popup-simple"));
+
+ QObject *popup = comboBox->property("popup").value<QObject *>();
+ QVERIFY(popup);
+ QVERIFY(qt_createdQObjects()->contains("combobox-popup-simple"));
+ }
+
+ reset();
+
+ {
+ // test that ComboBox::popup is created when it becomes visible
+ QQuickWindow window;
+ window.resize(300, 300);
+ window.show();
+ window.requestActivate();
+ QVERIFY(QTest::qWaitForWindowActive(&window));
+
+ QQmlComponent component(engine);
+ component.setData("import QtQuick.Controls; ComboBox { }", QUrl());
+ QScopedPointer<QQuickItem> comboBox(qobject_cast<QQuickItem *>(component.create()));
+ QVERIFY(comboBox);
+
+ comboBox->setParentItem(window.contentItem());
+ QVERIFY(!qt_createdQObjects()->contains("combobox-popup-simple"));
+
+ QTest::mouseClick(&window, Qt::LeftButton, Qt::NoModifier, QPoint(1, 1));
+ QVERIFY(qt_createdQObjects()->contains("combobox-popup-simple"));
+ }
+
+ reset();
+
+ {
+ // test that ComboBox::popup is completed upon component completion (if appropriate)
+ QQmlComponent component(engine);
+ component.setData("import QtQuick; import QtQuick.Controls; ComboBox { id: control; contentItem: Item { visible: !control.popup.visible } popup: Popup { property bool wasCompleted: false; Component.onCompleted: wasCompleted = true } }", QUrl());
+ QScopedPointer<QQuickItem> comboBox(qobject_cast<QQuickItem *>(component.create()));
+ QVERIFY(comboBox);
+
+ QObject *popup = comboBox->property("popup").value<QObject *>();
+ QVERIFY(popup);
+ QCOMPARE(popup->property("wasCompleted"), QVariant(true));
+ }
+}
+
+QTEST_MAIN(tst_customization)
+
+#include "tst_customization.moc"