// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include class tst_qqmlcontext : public QQmlDataTest { Q_OBJECT public: tst_qqmlcontext() : QQmlDataTest(QT_QMLTEST_DATADIR) {} private slots: void baseUrl(); void resolvedUrl(); void engineMethod(); void parentContext(); void setContextProperty(); void setContextProperties(); void setContextObject(); void destruction(); void idAsContextProperty(); void readOnlyContexts(); void objectsAndNames(); void refreshExpressions(); void refreshExpressionsCrash(); void refreshExpressionsRootContext(); void skipExpressionRefresh_qtbug_53431(); void qtbug_22535(); void evalAfterInvalidate(); void qobjectDerived(); void qtbug_49232(); void contextViaClosureAfterDestruction(); void contextLeak(); void importedScriptLookup(); void outerContextObject(); void contextObjectHierarchy(); void destroyContextProperty(); void destroyContextObject(); void numericContextProperty(); void gcDeletesContextObject(); private: QQmlEngine engine; }; void tst_qqmlcontext::baseUrl() { QQmlContext ctxt(&engine); QCOMPARE(ctxt.baseUrl(), QUrl()); ctxt.setBaseUrl(QUrl("http://www.qt-project.org/")); QCOMPARE(ctxt.baseUrl(), QUrl("http://www.qt-project.org/")); } void tst_qqmlcontext::resolvedUrl() { // Relative to the component { QQmlContext ctxt(&engine); ctxt.setBaseUrl(QUrl("http://www.qt-project.org/")); QCOMPARE(ctxt.resolvedUrl(QUrl("main.qml")), QUrl("http://www.qt-project.org/main.qml")); } // Relative to a parent { QQmlContext ctxt(&engine); ctxt.setBaseUrl(QUrl("http://www.qt-project.org/")); QQmlContext ctxt2(&ctxt); QCOMPARE(ctxt2.resolvedUrl(QUrl("main2.qml")), QUrl("http://www.qt-project.org/main2.qml")); } // Relative to the engine { QQmlContext ctxt(&engine); QCOMPARE(ctxt.resolvedUrl(QUrl("main.qml")), engine.baseUrl().resolved(QUrl("main.qml"))); } // Relative to a deleted parent { std::unique_ptr ctxt = std::make_unique(&engine); ctxt->setBaseUrl(QUrl("http://www.qt-project.org/")); QQmlContext ctxt2(ctxt.get()); QCOMPARE(ctxt2.resolvedUrl(QUrl("main2.qml")), QUrl("http://www.qt-project.org/main2.qml")); ctxt.reset(); QCOMPARE(ctxt2.resolvedUrl(QUrl("main2.qml")), QUrl()); } // Absolute { QQmlContext ctxt(&engine); QCOMPARE(ctxt.resolvedUrl(QUrl("http://www.qt-project.org/main2.qml")), QUrl("http://www.qt-project.org/main2.qml")); QCOMPARE(ctxt.resolvedUrl(QUrl("file:///main2.qml")), QUrl("file:///main2.qml")); } } void tst_qqmlcontext::engineMethod() { std::unique_ptr engine = std::make_unique(); QQmlContext ctxt(engine.get()); QQmlContext ctxt2(&ctxt); QQmlContext ctxt3(&ctxt2); QQmlContext ctxt4(&ctxt2); QCOMPARE(ctxt.engine(), engine.get()); QCOMPARE(ctxt2.engine(), engine.get()); QCOMPARE(ctxt3.engine(), engine.get()); QCOMPARE(ctxt4.engine(), engine.get()); engine.reset(); QCOMPARE(ctxt.engine(), engine.get()); QCOMPARE(ctxt2.engine(), engine.get()); QCOMPARE(ctxt3.engine(), engine.get()); QCOMPARE(ctxt4.engine(), engine.get()); } void tst_qqmlcontext::parentContext() { std::unique_ptr engine = std::make_unique(); QCOMPARE(engine->rootContext()->parentContext(), (QQmlContext *)nullptr); std::unique_ptr ctxt = std::make_unique(engine.get()); std::unique_ptr ctxt2 = std::make_unique(ctxt.get()); std::unique_ptr ctxt3 = std::make_unique(ctxt2.get()); std::unique_ptr ctxt4 = std::make_unique(ctxt2.get()); std::unique_ptr ctxt5 = std::make_unique(ctxt.get()); std::unique_ptr ctxt6 = std::make_unique(engine.get()); std::unique_ptr ctxt7 = std::make_unique(engine->rootContext()); QCOMPARE(ctxt->parentContext(), engine->rootContext()); QCOMPARE(ctxt2->parentContext(), ctxt.get()); QCOMPARE(ctxt3->parentContext(), ctxt2.get()); QCOMPARE(ctxt4->parentContext(), ctxt2.get()); QCOMPARE(ctxt5->parentContext(), ctxt.get()); QCOMPARE(ctxt6->parentContext(), engine->rootContext()); QCOMPARE(ctxt7->parentContext(), engine->rootContext()); ctxt2.reset(); QCOMPARE(ctxt->parentContext(), engine->rootContext()); QCOMPARE(ctxt3->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt4->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt5->parentContext(), ctxt.get()); QCOMPARE(ctxt6->parentContext(), engine->rootContext()); QCOMPARE(ctxt7->parentContext(), engine->rootContext()); engine.reset(); QCOMPARE(ctxt->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt3->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt4->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt5->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt6->parentContext(), (QQmlContext *)nullptr); QCOMPARE(ctxt7->parentContext(), (QQmlContext *)nullptr); } class TestObject : public QObject { Q_OBJECT Q_PROPERTY(int a READ a NOTIFY aChanged) Q_PROPERTY(int b READ b NOTIFY bChanged) Q_PROPERTY(int c READ c NOTIFY cChanged) Q_PROPERTY(char d READ d NOTIFY dChanged) Q_PROPERTY(uchar e READ e NOTIFY eChanged) public: TestObject() : _a(10), _b(10), _c(10) {} int a() const { return _a; } void setA(int a) { _a = a; emit aChanged(); } int b() const { return _b; } void setB(int b) { _b = b; emit bChanged(); } int c() const { return _c; } void setC(int c) { _c = c; emit cChanged(); } char d() const { return _d; } void setD(char d) { _d = d; emit dChanged(); } uchar e() const { return _e; } void setE(uchar e) { _e = e; emit eChanged(); } signals: void aChanged(); void bChanged(); void cChanged(); void dChanged(); void eChanged(); private: int _a; int _b; int _c; char _d; uchar _e; }; #define TEST_CONTEXT_PROPERTY(ctxt, name, value) \ { \ QQmlComponent component(&engine); \ component.setData("import QtQuick 2.0; QtObject { property variant test: " #name " }", QUrl()); \ \ std::unique_ptr obj { component.create(ctxt) }; \ \ QCOMPARE(obj->property("test"), value); \ } void tst_qqmlcontext::setContextProperty() { QQmlContext ctxt(&engine); QQmlContext ctxt2(&ctxt); TestObject obj1; obj1.setA(3345); TestObject obj2; obj2.setA(-19); // Static context properties ctxt.setContextProperty("a", QVariant(10)); ctxt.setContextProperty("b", QVariant(9)); ctxt2.setContextProperty("d", &obj2); ctxt2.setContextProperty("b", QVariant(19)); ctxt2.setContextProperty("c", QVariant(QString("Hello World!"))); ctxt.setContextProperty("d", &obj1); ctxt.setContextProperty("e", &obj1); TEST_CONTEXT_PROPERTY(&ctxt2, a, QVariant(10)); TEST_CONTEXT_PROPERTY(&ctxt2, b, QVariant(19)); TEST_CONTEXT_PROPERTY(&ctxt2, c, QVariant(QString("Hello World!"))); TEST_CONTEXT_PROPERTY(&ctxt2, d.a, QVariant(-19)); TEST_CONTEXT_PROPERTY(&ctxt2, e.a, QVariant(3345)); ctxt.setContextProperty("a", QVariant(13)); ctxt.setContextProperty("b", QVariant(4)); ctxt2.setContextProperty("b", QVariant(8)); ctxt2.setContextProperty("c", QVariant(QString("Hi World!"))); ctxt2.setContextProperty("d", &obj1); obj1.setA(12); TEST_CONTEXT_PROPERTY(&ctxt2, a, QVariant(13)); TEST_CONTEXT_PROPERTY(&ctxt2, b, QVariant(8)); TEST_CONTEXT_PROPERTY(&ctxt2, c, QVariant(QString("Hi World!"))); TEST_CONTEXT_PROPERTY(&ctxt2, d.a, QVariant(12)); TEST_CONTEXT_PROPERTY(&ctxt2, e.a, QVariant(12)); // Changes in context properties { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property variant test: a }", QUrl()); std::unique_ptr obj { component.create(&ctxt2) }; QCOMPARE(obj->property("test"), QVariant(13)); ctxt.setContextProperty("a", QVariant(19)); QCOMPARE(obj->property("test"), QVariant(19)); } { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property variant test: b }", QUrl()); std::unique_ptr obj { component.create(&ctxt2) }; QCOMPARE(obj->property("test"), QVariant(8)); ctxt.setContextProperty("b", QVariant(5)); QCOMPARE(obj->property("test"), QVariant(8)); ctxt2.setContextProperty("b", QVariant(1912)); QCOMPARE(obj->property("test"), QVariant(1912)); } { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property variant test: e.a }", QUrl()); std::unique_ptr obj { component.create(&ctxt2) }; QCOMPARE(obj->property("test"), QVariant(12)); obj1.setA(13); QCOMPARE(obj->property("test"), QVariant(13)); } // New context properties { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property variant test: a }", QUrl()); std::unique_ptr obj { component.create(&ctxt2) }; QCOMPARE(obj->property("test"), QVariant(19)); ctxt2.setContextProperty("a", QVariant(1945)); QCOMPARE(obj->property("test"), QVariant(1945)); } // Setting an object-variant context property { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { id: root; property int a: 10; property int test: ctxtProp.a; property variant obj: root; }", QUrl()); QQmlContext ctxt(engine.rootContext()); ctxt.setContextProperty("ctxtProp", QVariant()); QTest::ignoreMessage(QtWarningMsg, ":1: TypeError: Cannot read property 'a' of undefined"); std::unique_ptr obj { component.create(&ctxt) }; QVariant v = obj->property("obj"); ctxt.setContextProperty("ctxtProp", v); QCOMPARE(obj->property("test"), QVariant(10)); } } void tst_qqmlcontext::setContextProperties() { QQmlContext ctxt(&engine); TestObject obj1; obj1.setA(3345); TestObject obj2; obj2.setA(-19); QList properties; properties.append({QString("a"), QVariant(10)}); properties.append({QString("b"), QVariant(19)}); properties.append({QString("d"), QVariant::fromValue(&obj2)}); properties.append({QString("c"), QVariant(QString("Hello World!"))}); properties.append({QString("e"), QVariant::fromValue(&obj1)}); ctxt.setContextProperties(properties); TEST_CONTEXT_PROPERTY(&ctxt, a, QVariant(10)); TEST_CONTEXT_PROPERTY(&ctxt, b, QVariant(19)); TEST_CONTEXT_PROPERTY(&ctxt, c, QVariant(QString("Hello World!"))); TEST_CONTEXT_PROPERTY(&ctxt, d.a, QVariant(-19)); TEST_CONTEXT_PROPERTY(&ctxt, e.a, QVariant(3345)); } void tst_qqmlcontext::setContextObject() { QQmlContext ctxt(&engine); TestObject to; to.setA(2); to.setB(192); to.setC(18); ctxt.setContextObject(&to); ctxt.setContextProperty("c", QVariant(9)); // Static context properties TEST_CONTEXT_PROPERTY(&ctxt, a, QVariant(2)); TEST_CONTEXT_PROPERTY(&ctxt, b, QVariant(192)); TEST_CONTEXT_PROPERTY(&ctxt, c, QVariant(9)); to.setA(12); to.setB(100); to.setC(7); ctxt.setContextProperty("c", QVariant(3)); TEST_CONTEXT_PROPERTY(&ctxt, a, QVariant(12)); TEST_CONTEXT_PROPERTY(&ctxt, b, QVariant(100)); TEST_CONTEXT_PROPERTY(&ctxt, c, QVariant(3)); // Changes in context properties { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property variant test: a }", QUrl()); std::unique_ptr obj { component.create(&ctxt) }; QCOMPARE(obj->property("test"), QVariant(12)); to.setA(14); QCOMPARE(obj->property("test"), QVariant(14)); } // Change of context object ctxt.setContextProperty("c", QVariant(30)); TestObject to2; to2.setA(10); to2.setB(20); to2.setC(40); ctxt.setContextObject(&to2); TEST_CONTEXT_PROPERTY(&ctxt, a, QVariant(10)); TEST_CONTEXT_PROPERTY(&ctxt, b, QVariant(20)); TEST_CONTEXT_PROPERTY(&ctxt, c, QVariant(30)); } void tst_qqmlcontext::destruction() { std::unique_ptr ctxt = std::make_unique(&engine); QObject obj; QQmlEngine::setContextForObject(&obj, ctxt.get()); QQmlExpression expr(ctxt.get(), nullptr, "a"); QCOMPARE(ctxt.get(), QQmlEngine::contextForObject(&obj)); QCOMPARE(ctxt.get(), expr.context()); ctxt.reset(); QCOMPARE(ctxt.get(), QQmlEngine::contextForObject(&obj)); QCOMPARE(ctxt.get(), expr.context()); } void tst_qqmlcontext::idAsContextProperty() { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property variant a; a: QtObject { id: myObject } }", QUrl()); std::unique_ptr obj { component.create() }; QVERIFY(obj.get()); QVariant a = obj->property("a"); QCOMPARE(a.userType(), int(QMetaType::QObjectStar)); QVariant ctxt = qmlContext(obj.get())->contextProperty("myObject"); QCOMPARE(ctxt.userType(), int(QMetaType::QObjectStar)); QCOMPARE(a, ctxt); } // Internal contexts should be read-only void tst_qqmlcontext::readOnlyContexts() { QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { id: me }", QUrl()); std::unique_ptr obj { component.create() }; QVERIFY(obj.get()); QQmlContext *context = qmlContext(obj.get()); QVERIFY(context); QCOMPARE(qvariant_cast(context->contextProperty("me")), obj.get()); QCOMPARE(context->contextObject(), obj.get()); QTest::ignoreMessage(QtWarningMsg, "QQmlContext: Cannot set property on internal context."); context->setContextProperty("hello", 12); QCOMPARE(context->contextProperty("hello"), QVariant()); QTest::ignoreMessage(QtWarningMsg, "QQmlContext: Cannot set property on internal context."); context->setContextProperty("hello", obj.get()); QCOMPARE(context->contextProperty("hello"), QVariant()); QTest::ignoreMessage(QtWarningMsg, "QQmlContext: Cannot set context object for internal context."); context->setContextObject(nullptr); QCOMPARE(context->contextObject(), obj.get()); } void tst_qqmlcontext::objectsAndNames() { QObject o1; QObject o2; QObject o3; QQmlEngine engine; QQmlContext *rootContext = engine.rootContext(); // As a context property rootContext->setContextProperty(QStringLiteral("o1"), &o1); rootContext->setContextProperty(QStringLiteral("o2"), &o2); rootContext->setContextProperty(QStringLiteral("o1_2"), &o1); QCOMPARE(rootContext->nameForObject(&o1), QStringLiteral("o1")); QCOMPARE(rootContext->nameForObject(&o2), QStringLiteral("o2")); QCOMPARE(rootContext->nameForObject(&o3), QString()); QCOMPARE(rootContext->objectForName(QStringLiteral("o1")), &o1); QCOMPARE(rootContext->objectForName(QStringLiteral("o2")), &o2); QCOMPARE(rootContext->objectForName(QStringLiteral("o1_2")), &o1); QCOMPARE(rootContext->objectForName(QString()), nullptr); // As an id QQmlComponent component(&engine); component.setData("import QtQml\n" "QtObject {\n" " id: root\n" " property QtObject o: QtObject { id: nested }\n" "}", QUrl()); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer o(component.create()); QVERIFY(!o.isNull()); QQmlContext *context = qmlContext(o.data()); QCOMPARE(context->nameForObject(o.data()), QStringLiteral("root")); QCOMPARE(context->nameForObject(qvariant_cast(o->property("o"))), QStringLiteral("nested")); QCOMPARE(context->nameForObject(&o1), QString()); QCOMPARE(context->objectForName(QStringLiteral("root")), o.data()); QCOMPARE(context->objectForName(QStringLiteral("nested")), qvariant_cast(o->property("o"))); QCOMPARE(context->objectForName(QString()), nullptr); // From context object QQmlComponent ctxtComponent(&engine); ctxtComponent.setData("import QtQuick 6.1\n" "Image {\n" " property QtObject aa: QtObject { objectName: 'foo' }\n" " property QtObject bb: QtObject { objectName: 'bar' }\n" " property QtObject cc: QtObject { objectName: 'baz' }\n" " containmentMask: Rectangle {\n" " objectName: 'ddd'\n" " width: 10\n" " height: 20\n" " }\n" "}", QUrl()); QVERIFY2(ctxtComponent.isReady(), qPrintable(ctxtComponent.errorString())); QScopedPointer ctxtObj(ctxtComponent.create()); QVERIFY(!ctxtObj.isNull()); QScopedPointer extraContext(new QQmlContext(context)); extraContext->setContextObject(ctxtObj.data()); QObject *aa = qvariant_cast(ctxtObj->property("aa")); QCOMPARE(aa->objectName(), QStringLiteral("foo")); QObject *bb = qvariant_cast(ctxtObj->property("bb")); QCOMPARE(bb->objectName(), QStringLiteral("bar")); QObject *cc = qvariant_cast(ctxtObj->property("cc")); QCOMPARE(cc->objectName(), QStringLiteral("baz")); QObject *containmentMask = qvariant_cast(ctxtObj->property("containmentMask")); QCOMPARE(containmentMask->objectName(), QStringLiteral("ddd")); QCOMPARE(extraContext->nameForObject(aa), QStringLiteral("aa")); QCOMPARE(extraContext->nameForObject(bb), QStringLiteral("bb")); QCOMPARE(extraContext->nameForObject(cc), QStringLiteral("cc")); QCOMPARE(extraContext->nameForObject(containmentMask), QStringLiteral("containmentMask")); QCOMPARE(extraContext->objectForName(QStringLiteral("aa")), aa); QCOMPARE(extraContext->objectForName(QStringLiteral("bb")), bb); QCOMPARE(extraContext->objectForName(QStringLiteral("cc")), cc); QCOMPARE(extraContext->objectForName(QStringLiteral("containmentMask")), containmentMask); QCOMPARE(extraContext->contextProperty(QStringLiteral("aa")), QVariant::fromValue(aa)); QCOMPARE(extraContext->contextProperty(QStringLiteral("bb")), QVariant::fromValue(bb)); QCOMPARE(extraContext->contextProperty(QStringLiteral("cc")), QVariant::fromValue(cc)); QCOMPARE(extraContext->contextProperty(QStringLiteral("containmentMask")), QVariant::fromValue(containmentMask)); // Context properties travel the context hierarchy QCOMPARE(extraContext->contextProperty(QStringLiteral("root")), QVariant::fromValue(o.data())); QCOMPARE(extraContext->contextProperty(QStringLiteral("nested")), o->property("o")); // objectForName and nameForObject deliberately don't QCOMPARE(extraContext->objectForName(QStringLiteral("root")), nullptr); QCOMPARE(extraContext->objectForName(QStringLiteral("nested")), nullptr); QCOMPARE(extraContext->nameForObject(o.data()), QString()); QCOMPARE(extraContext->nameForObject(qvariant_cast(o->property("o"))), QString()); } class DeleteCommand : public QObject { Q_OBJECT public: DeleteCommand() : object(nullptr) {} QObject *object; public slots: void doCommand() { if (object) delete object; object = nullptr; } }; // Calling refresh expressions would crash if an expression or context was deleted during // the refreshing void tst_qqmlcontext::refreshExpressionsCrash() { { QQmlEngine engine; DeleteCommand command; engine.rootContext()->setContextProperty("deleteCommand", &command); // We use a fresh context here to bypass any root-context optimizations in // the engine QQmlContext ctxt(engine.rootContext()); QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property var binding: deleteCommand.doCommand() }", QUrl()); QVERIFY(component.isReady()); std::unique_ptr o1 { component.create(&ctxt) }; QObject *o2 = component.create(&ctxt); command.object = o2; QQmlContextData::get(&ctxt)->refreshExpressions(); } { QQmlEngine engine; DeleteCommand command; engine.rootContext()->setContextProperty("deleteCommand", &command); // We use a fresh context here to bypass any root-context optimizations in // the engine QQmlContext ctxt(engine.rootContext()); QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property var binding: deleteCommand.doCommand() }", QUrl()); QVERIFY(component.isReady()); QObject *o1 = component.create(&ctxt); std::unique_ptr o2 { component.create(&ctxt) }; command.object = o1; QQmlContextData::get(&ctxt)->refreshExpressions(); } } class CountCommand : public QObject { Q_OBJECT public: CountCommand() : count(0) {} int count; public slots: void doCommand() { ++count; } }; // Test that calling refresh expressions causes all the expressions to refresh void tst_qqmlcontext::refreshExpressions() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("refreshExpressions.qml")); QQmlComponent component2(&engine, testFileUrl("RefreshExpressionsType.qml")); CountCommand command; engine.rootContext()->setContextProperty("countCommand", &command); // We use a fresh context here to bypass any root-context optimizations in // the engine QQmlContext context(engine.rootContext()); QQmlContext context2(&context); std::unique_ptr o1 { component.create(&context) }; std::unique_ptr o2 { component.create(&context2) }; std::unique_ptr o3 { component2.create(&context) }; QCOMPARE(command.count, 5); QQmlContextData::get(&context)->refreshExpressions(); QCOMPARE(command.count, 10); } // Test that updating the root context, only causes expressions in contexts with an // unresolved name to reevaluate void tst_qqmlcontext::refreshExpressionsRootContext() { QQmlEngine engine; CountCommand command; engine.rootContext()->setContextProperty("countCommand", &command); QQmlComponent component(&engine, testFileUrl("refreshExpressions.qml")); QQmlComponent component2(&engine, testFileUrl("refreshExpressionsRootContext.qml")); QQmlContext context(engine.rootContext()); QQmlContext context2(engine.rootContext()); QString warning = component2.url().toString() + QLatin1String(":4: ReferenceError: unresolvedName is not defined"); std::unique_ptr o1 { component.create(&context) }; QTest::ignoreMessage(QtWarningMsg, qPrintable(warning)); std::unique_ptr o2 { component2.create(&context2) }; QCOMPARE(command.count, 3); QTest::ignoreMessage(QtWarningMsg, qPrintable(warning)); QQmlContextData::get(engine.rootContext())->refreshExpressions(); QCOMPARE(command.count, 4); } void tst_qqmlcontext::skipExpressionRefresh_qtbug_53431() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("qtbug_53431.qml")); QScopedPointer object(component.create(nullptr)); QVERIFY(!object.isNull()); QCOMPARE(object->property("value").toInt(), 1); object->setProperty("value", 10); QCOMPARE(object->property("value").toInt(), 10); engine.rootContext()->setContextProperty("randomContextProperty", 42); QCOMPARE(object->property("value").toInt(), 10); } void tst_qqmlcontext::qtbug_22535() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("qtbug_22535.qml")); QQmlContext context(engine.rootContext()); std::unique_ptr o { component.create(&context) }; // Don't crash! } void tst_qqmlcontext::evalAfterInvalidate() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("evalAfterInvalidate.qml")); QScopedPointer o(component.create()); QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); QCoreApplication::processEvents(); } void tst_qqmlcontext::qobjectDerived() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("refreshExpressions.qml")); CountCommand command; // This test is similar to refreshExpressions, but with the key difference that // we use the QVariant overload of setContextProperty. That way, we test that // QVariant knowledge that it contains a QObject derived pointer is used. engine.rootContext()->setContextProperty("countCommand", QVariant::fromValue(&command)); // We use a fresh context here to bypass any root-context optimizations in // the engine QQmlContext context(engine.rootContext()); QScopedPointer o1(component.create(&context)); Q_UNUSED(o1); QCOMPARE(command.count, 2); } void tst_qqmlcontext::qtbug_49232() { TestObject testObject; testObject.setD('a'); testObject.setE(97); QQmlEngine engine; engine.rootContext()->setContextProperty("TestObject", &testObject); QQmlComponent component(&engine); component.setData("import QtQuick 2.0; QtObject { property int valueOne: TestObject.d; property int valueTwo: TestObject.e }", QUrl()); QScopedPointer obj(component.create()); QCOMPARE(obj->property("valueOne"), QVariant('a')); QCOMPARE(obj->property("valueTwo"), QVariant(97)); } void tst_qqmlcontext::contextViaClosureAfterDestruction() { qmlRegisterSingletonType(testFileUrl("Singleton.qml"), "constants", 1, 0, "Sing"); QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("contextViaClosureAfterDestruction.qml")); QJSValue valueClosure; QJSValue componentFactoryClosure; { QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); // meta-calls don't support QJSValue return types, so do the call "by hand" valueClosure = engine.newQObject(obj.data()).property(QStringLiteral("createClosure")).call(); QVERIFY(valueClosure.isCallable()); componentFactoryClosure = engine.newQObject(obj.data()).property(QStringLiteral("createComponentFactory")).call(); QVERIFY(componentFactoryClosure.isCallable()); } QCOMPARE(valueClosure.call().toString(), QLatin1String("Highway to Hell")); QScopedPointer parent(new QObject); QJSValue parentWrapper = engine.newQObject(parent.data()); QQmlEngine::setObjectOwnership(parent.data(), QQmlEngine::CppOwnership); QJSValue subObject = componentFactoryClosure.callWithInstance(componentFactoryClosure, QJSValueList() << parentWrapper); QVERIFY(subObject.isError()); QCOMPARE(subObject.toString(), QLatin1String("Error: Qt.createQmlObject(): Cannot create a component in an invalid context")); } void tst_qqmlcontext::contextLeak() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("contextLeak.qml")); QQmlGuardedContextData scriptContext; { QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QCOMPARE(obj->property("value").toInt(), 42); QQmlData *ddata = QQmlData::get(obj.data()); QVERIFY(ddata); QQmlRefPointer context = ddata->context; QVERIFY(context); QVERIFY(!context->importedScripts().isNullOrUndefined()); QCOMPARE(int(context->importedScripts().valueRef()->as()->getLength()), 1); QV4::Scope scope(ddata->jsWrapper.engine()); QV4::ScopedValue scriptContextWrapper(scope); scriptContextWrapper = context->importedScripts().valueRef() ->as()->get(uint(0)); scriptContext = scriptContextWrapper->as()->getContext(); } gc(engine); // Each time a JS file (non-pragma-shared) is imported, we create a QQmlContext(Data) for it. // Make sure that context does not leak. // The QQmlGuardedContextData also holds a reference. Therefore, the refCount is still 1. // All other references should be gone by now. QCOMPARE(scriptContext->refCount(), 1); } void tst_qqmlcontext::importedScriptLookup() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("contextLeak.qml")); QScopedPointer obj(component.create()); QVERIFY(!obj.isNull()); QJSValue script = qmlContext(obj.data())->importedScript("ContextLeak"); QCOMPARE(script.property("value").toInt(), 42); } static bool buildObjectList(QQmlContext *ctxt) { static QHash deletedObjects; QQmlRefPointer p = QQmlContextData::get(ctxt); QObject *object = p->contextObject(); if (object) { // If the object was actually deleted this is likely to crash in one way or another. // Either the memory is still intact, then we will probably find the objectName, or it is // not, then the connect() below is likely to fail. if (deletedObjects.contains(object) && deletedObjects[object] == object->objectName()) return false; QObject::connect(object, &QObject::destroyed, [object]() { object->setObjectName(QString::number(deletedObjects.size())); deletedObjects.insert(object, object->objectName()); }); } QQmlRefPointer child = p->childContexts(); while (child) { if (!buildObjectList(child->asQQmlContext())) return false; child = child->nextChild(); } return true; } void tst_qqmlcontext::outerContextObject() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("outerContextObject.qml")); QVERIFY(component.isReady()); QScopedPointer object(component.create()); QVERIFY(!object.isNull()); int iterations = 0; QTimer timer; timer.setInterval(1); QObject::connect(&timer, &QTimer::timeout, &engine, [&]() { if (!buildObjectList(engine.rootContext())) { iterations = 100; timer.stop(); QFAIL("Deleted object found as context object"); } else { ++iterations; } }); timer.start(); QTRY_VERIFY(iterations >= 100); } void tst_qqmlcontext::contextObjectHierarchy() { QQmlEngine engine; QScopedPointer root; { // Drop the component after create(), to release the root context. QQmlComponent component(&engine); component.loadUrl(testFileUrl("contextObjectHierarchy.qml")); QVERIFY(component.isReady()); root.reset(component.create()); } QVERIFY(!root.isNull()); for (const QObject *child : root->children()) QVERIFY(QQmlData::get(child)->outerContext != nullptr); connect(root.data(), &QObject::destroyed, [&root]() { for (const QObject *child : root->children()) QCOMPARE(QQmlData::get(child)->outerContext, nullptr); }); } void tst_qqmlcontext::destroyContextProperty() { QScopedPointer context; QScopedPointer objectThatOutlivesEngine(new QObject); { QQmlEngine engine; context.reset(new QQmlContext(&engine)); { QObject object; context->setContextProperty(QLatin1String("a"), &object); QCOMPARE(qvariant_cast(context->contextProperty(QLatin1String("a"))), &object); } QCOMPARE(qvariant_cast(context->contextProperty(QLatin1String("a"))), nullptr); context->setContextProperty(QLatin1String("b"), objectThatOutlivesEngine.data()); } // dropDestroyedObject() should not crash, even if the engine is gone. objectThatOutlivesEngine.reset(); // We're not allowed to call context->contextProperty("b") anymore. // TODO: Or are we? } void tst_qqmlcontext::destroyContextObject() { QQmlEngine engine; QList> contexts; QQmlComponent component(&engine, testFileUrl("destroyContextObject.qml")); QScopedPointer root(component.create()); QPointer a = root->property("a").value(); QVERIFY(a); for (QQmlRefPointer context = QQmlData::get(a)->ownContext; context; context = context->parent()) { contexts.append(context); } QObject *deleted = a.data(); root.reset(); QVERIFY(a.isNull()); for (const auto &context : contexts) QVERIFY(context->contextObject() != deleted); } void tst_qqmlcontext::numericContextProperty() { QQmlEngine engine; auto context = engine.rootContext(); QTest::ignoreMessage(QtWarningMsg, "QQmlContext: Using numbers as context properties will be disallowed in a future Qt version."); context->setContextProperty(QLatin1String("11"), 42); QCOMPARE(context->contextProperty(QLatin1String("11")).toInt(), 42); } void tst_qqmlcontext::gcDeletesContextObject() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("gcDeletesContextObject.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QPointer contextObject = o->property("o").value(); QVERIFY(contextObject != nullptr); QQmlData *data = QQmlData::get(contextObject); QVERIFY(data); QQmlRefPointer context = data->ownContext; QVERIFY(context); QCOMPARE(context->contextObject(), contextObject); o->setProperty("o", QVariant::fromValue(nullptr)); QCOMPARE(o->property("o").value(), nullptr); engine.collectGarbage(); QTRY_VERIFY(contextObject.isNull()); QCOMPARE(context->contextObject(), nullptr); } QTEST_MAIN(tst_qqmlcontext) #include "tst_qqmlcontext.moc"