qtdeclarative/tests/auto/qml/qqmlengine/tst_qqmlengine.cpp

1910 lines
66 KiB
C++
Raw Normal View History

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QQmlEngine>
#include <QQmlContext>
#include <QNetworkAccessManager>
#include <QPointer>
#include <QDir>
#include <QStandardPaths>
#include <QSignalSpy>
#include <QDebug>
#include <QBuffer>
#include <QCryptographicHash>
#include <QQmlComponent>
#include <QQmlNetworkAccessManagerFactory>
#include <QQmlExpression>
#include <QQmlIncubationController>
#include <QTemporaryDir>
#include <QQmlEngineExtensionPlugin>
#include <private/qqmlengine_p.h>
#include <private/qqmltypedata_p.h>
#include <private/qqmlcomponentattached_p.h>
#include <QQmlAbstractUrlInterceptor>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include "declarativelyregistered.h"
Q_IMPORT_QML_PLUGIN(OnlyDeclarativePlugin)
class tst_qqmlengine : public QQmlDataTest
{
Q_OBJECT
public:
tst_qqmlengine() : QQmlDataTest(QT_QMLTEST_DATADIR) {}
private slots:
void initTestCase() override;
void rootContext();
#if QT_CONFIG(qml_network)
void networkAccessManager();
void synchronousNetworkAccessManager();
#endif
void baseUrl();
void contextForObject();
void offlineStoragePath();
void offlineDatabaseStoragePath();
void clearComponentCache();
void trimComponentCache();
void trimComponentCache_data();
void clearSingletons();
void repeatedCompilation();
void failedCompilation();
void failedCompilation_data();
void outputWarningsToStandardError();
void objectOwnership();
void multipleEngines();
void qtqmlModule_data();
void qtqmlModule();
void urlInterceptor_data();
void urlInterceptor();
void qmlContextProperties();
void testGCCorruption();
void testGroupedPropertyRevisions();
void componentFromEval();
void qrcUrls();
void cppSignalAndEval();
void singletonInstance();
void aggressiveGc();
void cachedGetterLookup_qtbug_75335();
void createComponentOnSingletonDestruction();
void uiLanguage();
void markCurrentFunctionAsTranslationBinding();
void executeRuntimeFunction();
void captureQProperty();
void listWrapperAsListReference();
void attachedObjectAsObject();
void listPropertyAsQJSValue();
void stringToColor();
void qobjectToString();
void qtNamespaceInQtObject();
void nativeModuleImport();
Allow limited extensions to globals We can allow a, overriding data members of globals, such as Error.name b, adding members that don't clash with any internals c, any manipulation of toString(), toLocaleString(), valueOf(), and constructor To that effect, add a "Locked" flag to our internal classes. If that is set, disallow changing prototypes and when defining a property, check if it shadows any non-configurable property. Furthermore, make all non-primitive properties that are not meant to be overridden non-configurable and non-writable. constructor, toString(), toLocaleString() and valueOf() are exempt because they are explicitly meant to be overridden by users. Therefore, we let that happen and refrain from optimizing them or triggering their implicit invocation in optimized code. [ChangeLog][QtQml][Important Behavior Changes] The JavaScript global objects are not frozen anymore in a QML engine. Instead, they are selectively locked. You can extend the objects with new members as long as you don't shadow any existing methods, and you can change or override data members. This also means that most methods of Object.prototype, which was previously exempt from the freezing, cannot be changed anymore. You can, however, change or override constructor, toString(), toLocaleString() and valueOf() on any prototype. Those are clearly meant to be overridden by user code. Fixes: QTBUG-101298 Task-number: QTBUG-84341 Change-Id: Id77db971f76c8f48b18e7a93607da5f947ecfc3e Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
2022-08-29 10:00:52 +00:00
void lockedRootObject();
void crossReferencingSingletonsDeletion();
void bindingInstallUseAfterFree();
void objectListArgumentMethod();
void variantListQJsonConversion();
void attachedObjectOfUnregistered();
void dropCUOnEngineShutdown();
void multiLoadedJavaScriptModule();
void metaObjectOfScriptCU();
void registerModule();
public slots:
QObject *createAQObjectForOwnershipTest ()
{
static QObject *ptr = new QObject();
return ptr;
}
private:
QTemporaryDir m_tempDir;
};
class ObjectCaller : public QObject
{
Q_OBJECT
signals:
void doubleReply(const double a);
};
class CppSingleton : public QObject {
Q_OBJECT
public:
static uint instantiations;
uint id = 0;
CppSingleton() : id(++instantiations) {}
static QObject *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
{
Q_UNUSED(qmlEngine);
Q_UNUSED(jsEngine);
return new CppSingleton();
}
};
uint CppSingleton::instantiations = 0;
class JsSingleton : public QObject {
Q_OBJECT
public:
static uint instantiations;
uint id = 0;
JsSingleton() : id(++instantiations) {}
static QJSValue create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
{
Q_UNUSED(qmlEngine);
QJSValue value = jsEngine->newQObject(new JsSingleton());
return value;
}
};
uint JsSingleton::instantiations = 0;
void tst_qqmlengine::initTestCase()
{
QVERIFY2(m_tempDir.isValid(), qPrintable(m_tempDir.errorString()));
QQmlDataTest::initTestCase();
}
void tst_qqmlengine::rootContext()
{
QQmlEngine engine;
QVERIFY(engine.rootContext());
QCOMPARE(engine.rootContext()->engine(), &engine);
QVERIFY(!engine.rootContext()->parentContext());
}
#if QT_CONFIG(qml_network)
class NetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
public:
NetworkAccessManagerFactory() : manager(nullptr) {}
QNetworkAccessManager *create(QObject *parent) override {
manager = new QNetworkAccessManager(parent);
return manager;
}
QNetworkAccessManager *manager;
};
void tst_qqmlengine::networkAccessManager()
{
std::unique_ptr<QQmlEngine> engine = std::make_unique<QQmlEngine>();
// Test QQmlEngine created manager
QPointer<QNetworkAccessManager> manager = engine->networkAccessManager();
QVERIFY(manager != nullptr);
// Test factory created manager
engine.reset(new QQmlEngine);
NetworkAccessManagerFactory factory;
engine->setNetworkAccessManagerFactory(&factory);
QCOMPARE(engine->networkAccessManagerFactory(), &factory);
QNetworkAccessManager *engineNam = engine->networkAccessManager(); // calls NetworkAccessManagerFactory::create()
QCOMPARE(engineNam, factory.manager);
}
class ImmediateReply : public QNetworkReply {
Q_OBJECT
public:
ImmediateReply() {
setFinished(true);
}
qint64 readData(char* , qint64 ) override {
return 0;
}
void abort() override { }
};
class ImmediateManager : public QNetworkAccessManager {
Q_OBJECT
public:
ImmediateManager(QObject *parent = nullptr) : QNetworkAccessManager(parent) {
}
QNetworkReply *createRequest(Operation, const QNetworkRequest & , QIODevice * outgoingData = nullptr) override
{
Q_UNUSED(outgoingData);
return new ImmediateReply;
}
};
class ImmediateFactory : public QQmlNetworkAccessManagerFactory {
public:
QNetworkAccessManager *create(QObject *) override
{ return new ImmediateManager; }
};
void tst_qqmlengine::synchronousNetworkAccessManager()
{
ImmediateFactory factory;
QQmlEngine engine;
engine.setNetworkAccessManagerFactory(&factory);
QQmlComponent c(&engine, QUrl("myScheme://test.qml"));
// reply is finished, so should not be in loading state.
QVERIFY(!c.isLoading());
}
#endif
void tst_qqmlengine::baseUrl()
{
QQmlEngine engine;
QUrl cwd = QUrl::fromLocalFile(QDir::currentPath() + QDir::separator());
QCOMPARE(engine.baseUrl(), cwd);
QCOMPARE(engine.rootContext()->resolvedUrl(QUrl("main.qml")), cwd.resolved(QUrl("main.qml")));
QDir dir = QDir::current();
dir.cdUp();
QVERIFY(dir != QDir::current());
QDir::setCurrent(dir.path());
QCOMPARE(QDir::current(), dir);
QUrl cwd2 = QUrl::fromLocalFile(QDir::currentPath() + QDir::separator());
QCOMPARE(engine.baseUrl(), cwd2);
QCOMPARE(engine.rootContext()->resolvedUrl(QUrl("main.qml")), cwd2.resolved(QUrl("main.qml")));
engine.setBaseUrl(cwd);
QCOMPARE(engine.baseUrl(), cwd);
QCOMPARE(engine.rootContext()->resolvedUrl(QUrl("main.qml")), cwd.resolved(QUrl("main.qml")));
const QString testPath = QDir::currentPath() + QLatin1String("/");
const QString rootPath = QDir::rootPath();
engine.setBaseUrl(QUrl());
// Check that baseUrl returns a url to a localFile
QCOMPARE(engine.baseUrl().toLocalFile(), testPath);
QDir::setCurrent(QDir::rootPath());
// Make sure this also works when in the rootPath
QCOMPARE(engine.baseUrl().toLocalFile(), rootPath);
}
void tst_qqmlengine::contextForObject()
{
std::unique_ptr<QQmlEngine> engine = std::make_unique<QQmlEngine>();
// Test null-object
QVERIFY(!QQmlEngine::contextForObject(nullptr));
// Test an object with no context
QObject object;
QVERIFY(!QQmlEngine::contextForObject(&object));
// Test setting null-object
QQmlEngine::setContextForObject(nullptr, engine->rootContext());
// Test setting null-context
QQmlEngine::setContextForObject(&object, nullptr);
// Test setting context
QQmlEngine::setContextForObject(&object, engine->rootContext());
QCOMPARE(QQmlEngine::contextForObject(&object), engine->rootContext());
QQmlContext context(engine->rootContext());
// Try changing context
QTest::ignoreMessage(QtWarningMsg, "QQmlEngine::setContextForObject(): Object already has a QQmlContext");
QQmlEngine::setContextForObject(&object, &context);
QCOMPARE(QQmlEngine::contextForObject(&object), engine->rootContext());
// Delete context
engine.reset();
QVERIFY(!QQmlEngine::contextForObject(&object));
}
void tst_qqmlengine::offlineStoragePath()
{
// Without these set, QDesktopServices::storageLocation returns
// strings with extra "//" at the end. We set them to ignore this problem.
qApp->setApplicationName("tst_qqmlengine");
qApp->setOrganizationName("QtProject");
qApp->setOrganizationDomain("www.qt-project.org");
QQmlEngine engine;
QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QCOMPARE(dataLocation.isEmpty(), engine.offlineStoragePath().isEmpty());
QDir dir(dataLocation);
dir.mkpath("QML");
dir.cd("QML");
dir.mkpath("OfflineStorage");
dir.cd("OfflineStorage");
QCOMPARE(QDir::fromNativeSeparators(engine.offlineStoragePath()), dir.path());
QSignalSpy offlineStoragePathSpy(&engine, &QQmlEngine::offlineStoragePathChanged);
engine.setOfflineStoragePath(QDir::homePath());
QCOMPARE(offlineStoragePathSpy.size(), 1);
QCOMPARE(engine.offlineStoragePath(), QDir::homePath());
}
void tst_qqmlengine::offlineDatabaseStoragePath()
{
// Without these set, QDesktopServices::storageLocation returns
// strings with extra "//" at the end. We set them to ignore this problem.
qApp->setApplicationName("tst_qqmlengine");
qApp->setOrganizationName("QtProject");
qApp->setOrganizationDomain("www.qt-project.org");
QQmlEngine engine;
QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
const QString databaseName = QLatin1String("foo");
QString databaseLocation = engine.offlineStorageDatabaseFilePath(databaseName);
QCOMPARE(dataLocation.isEmpty(), databaseLocation.isEmpty());
QDir dir(dataLocation);
dir.mkpath("QML");
dir.cd("QML");
dir.mkpath("OfflineStorage");
dir.cd("OfflineStorage");
dir.mkpath("Databases");
dir.cd("Databases");
QCOMPARE(QFileInfo(databaseLocation).dir().path(), dir.path());
QCryptographicHash md5(QCryptographicHash::Md5);
md5.addData(databaseName.toUtf8());
QCOMPARE(databaseLocation, QDir::toNativeSeparators(dir.filePath(QLatin1String(md5.result().toHex()))));
}
void tst_qqmlengine::clearComponentCache()
{
QQmlEngine engine;
const QString fileName = m_tempDir.filePath(QStringLiteral("temp.qml"));
const QUrl fileUrl = QUrl::fromLocalFile(fileName);
// Create original qml file
{
QFile file(fileName);
QVERIFY(file.open(QIODevice::WriteOnly));
file.write("import QtQuick 2.0\nQtObject {\nproperty int test: 10\n}\n");
file.close();
}
// Test "test" property
{
QQmlComponent component(&engine, fileUrl);
std::unique_ptr<QObject> obj { component.create() };
QVERIFY(obj.get() != nullptr);
QCOMPARE(obj->property("test").toInt(), 10);
}
// Modify qml file
{
// On macOS with HFS+ the precision of file times is measured in seconds, so to ensure that
// the newly written file has a modification date newer than an existing cache file, we must
// wait.
// Similar effects of lacking precision have been observed on some Linux systems.
QThread::sleep(1);
QFile file(fileName);
QVERIFY(file.open(QIODevice::WriteOnly));
file.write("import QtQuick 2.0\nQtObject {\nproperty int test: 11\n}\n");
file.close();
}
// Test cache hit
{
QQmlComponent component(&engine, fileUrl);
std::unique_ptr<QObject> obj { component.create() };
QVERIFY(obj.get() != nullptr);
QCOMPARE(obj->property("test").toInt(), 10);
}
// Clear cache
engine.clearComponentCache();
// Nothing holds on to any CU anymore. They should all be gone.
QVERIFY(QQmlEnginePrivate::get(&engine)->v4engine()->compilationUnits().isEmpty());
// Test cache refresh
{
QQmlComponent component(&engine, fileUrl);
std::unique_ptr<QObject> obj { component.create() };
QVERIFY(obj.get() != nullptr);
QCOMPARE(obj->property("test").toInt(), 11);
engine.clearComponentCache();
// The CU we are holding on to is still alive.
// Otherwise we cannot mark its objects for GC anymore.
QVERIFY(!QQmlEnginePrivate::get(&engine)->v4engine()->compilationUnits().isEmpty());
}
// Regular Synchronous loading will leave us with an event posted
// to the gui thread and an extra refcount that will only be dropped after the
// event delivery. Call sendPostedEvents() to get rid of it so that
// the temporary directory can be removed.
QCoreApplication::sendPostedEvents();
}
struct ComponentCacheFunctions : public QObject, public QQmlIncubationController
{
Q_OBJECT
public:
QQmlEngine *engine;
ComponentCacheFunctions(QQmlEngine &e) : engine(&e) {}
Q_INVOKABLE void trim()
{
// Wait for any pending deletions to occur
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QCoreApplication::processEvents();
// There might be JS function objects around that hold a last ref to the compilation unit that's
// keeping the type compilation data (CompilationUnit) around. Let's collect them as well so that
// trim works well.
gc(*engine);
engine->trimComponentCache();
}
Q_INVOKABLE bool isTypeLoaded(QString file)
{
return QQmlTypeLoader::get(engine)
->isTypeLoaded(tst_qqmlengine::instance()->testFileUrl(file));
}
Q_INVOKABLE bool isScriptLoaded(QString file)
{
return QQmlTypeLoader::get(engine)
->isScriptLoaded(tst_qqmlengine::instance()->testFileUrl(file));
}
Q_INVOKABLE void beginIncubation()
{
startTimer(0);
}
Q_INVOKABLE void waitForIncubation()
{
while (incubatingObjectCount() > 0) {
QCoreApplication::processEvents();
}
}
private:
void timerEvent(QTimerEvent *) override
{
incubateFor(1000);
}
};
void tst_qqmlengine::trimComponentCache()
{
QFETCH(QString, file);
QQmlEngine engine;
ComponentCacheFunctions componentCache(engine);
engine.setIncubationController(&componentCache);
QQmlComponent component(&engine, testFileUrl(file));
QVERIFY2(component.isReady(), qPrintable(component.errorString()));
QScopedPointer<QObject> object(component.createWithInitialProperties({
{"componentCache", QVariant::fromValue(&componentCache)}
}));
QVERIFY(object != nullptr);
QCOMPARE(object->property("success").toBool(), true);
}
void tst_qqmlengine::trimComponentCache_data()
{
QTest::addColumn<QString>("file");
// The various tests here are for two types of components: those that are
// empty apart from their inherited elements, and those that define new properties.
// For each there are five types of composition: extension, aggregation,
// aggregation via component, property and object-created-via-transient-component.
const QStringList components = {
"EmptyComponent",
"VMEComponent",
"EmptyExtendEmptyComponent",
"VMEExtendEmptyComponent",
"EmptyExtendVMEComponent",
"VMEExtendVMEComponent",
"EmptyAggregateEmptyComponent",
"VMEAggregateEmptyComponent",
"EmptyAggregateVMEComponent",
"VMEAggregateVMEComponent",
"EmptyPropertyEmptyComponent",
"VMEPropertyEmptyComponent",
"EmptyPropertyVMEComponent",
"VMEPropertyVMEComponent",
"VMETransientEmptyComponent",
"VMETransientVMEComponent",
};
for (const QString &test : components) {
// For these cases, we first test that the component instance keeps the components
// referenced, and then that the instantiated object keeps the components referenced
for (int i = 1; i <= 2; ++i) {
QString name(QString("%1-%2").arg(test).arg(i));
QString file(QString("test%1.%2.qml").arg(test).arg(i));
QTest::newRow(name.toLatin1().constData()) << file;
}
}
// Test that a transient component is correctly referenced
QTest::newRow("TransientComponent-1") << "testTransientComponent.1.qml";
QTest::newRow("TransientComponent-2") << "testTransientComponent.2.qml";
// Test that components can be reloaded after unloading
QTest::newRow("ReloadComponent") << "testReloadComponent.qml";
// Test that components are correctly referenced when dynamically loaded
QTest::newRow("LoaderComponent") << "testLoaderComponent.qml";
// Test that components are correctly referenced when incubated
QTest::newRow("IncubatedComponent") << "testIncubatedComponent.qml";
// Test that a top-level omponents is correctly referenced
QTest::newRow("TopLevelComponent") << "testTopLevelComponent.qml";
// TODO:
// Test that scripts are unloaded when no longer referenced
QTest::newRow("ScriptComponent") << "testScriptComponent.qml";
}
static QJSValue createValueSingleton(QQmlEngine *, QJSEngine *) { return 13u; }
void tst_qqmlengine::clearSingletons()
{
ObjectCaller objectCaller1;
ObjectCaller objectCaller2;
const int cppInstance = qmlRegisterSingletonInstance(
"ClearSingletons", 1, 0, "CppInstance", &objectCaller1);
#if QT_DEPRECATED_SINCE(6, 3)
QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED
QQmlPrivate::SingletonFunctor deprecatedSingletonFunctor;
deprecatedSingletonFunctor.m_object = &objectCaller2;
const int deprecatedCppInstance = qmlRegisterSingletonType<ObjectCaller>(
"ClearSingletons", 1, 0, "DeprecatedCppInstance", deprecatedSingletonFunctor);
QT_WARNING_POP
#endif // QT_DEPRECATED_SINCE(6, 3)
const int cppFactory = qmlRegisterSingletonType<CppSingleton>(
"ClearSingletons", 1, 0, "CppFactory", &CppSingleton::create);
const int jsValue = qmlRegisterSingletonType(
"ClearSingletons", 1, 0, "JsValue", &createValueSingleton);
const int jsObject = qmlRegisterSingletonType(
"ClearSingletons", 1, 0, "JsObject", &JsSingleton::create);
const int qmlObject = qmlRegisterSingletonType(
testFileUrl("QmlSingleton.qml"), "ClearSingletons", 1, 0, "QmlSingleton");
QQmlEngine engine;
QCOMPARE(engine.singletonInstance<ObjectCaller *>(cppInstance), &objectCaller1);
#if QT_DEPRECATED_SINCE(6, 3)
QCOMPARE(engine.singletonInstance<ObjectCaller *>(deprecatedCppInstance), &objectCaller2);
#endif
const CppSingleton *oldCppSingleton = engine.singletonInstance<CppSingleton *>(cppFactory);
QVERIFY(oldCppSingleton != nullptr);
const uint oldCppSingletonId = oldCppSingleton->id;
QVERIFY(oldCppSingletonId > 0);
QCOMPARE(CppSingleton::instantiations, oldCppSingletonId);
QCOMPARE(engine.singletonInstance<QJSValue>(jsValue).toUInt(), 13u);
const JsSingleton *oldJsSingleton = engine.singletonInstance<JsSingleton *>(jsObject);
QVERIFY(oldJsSingleton != nullptr);
const uint oldJsSingletonId = oldJsSingleton->id;
const QObject *oldQmlSingleton = engine.singletonInstance<QObject *>(qmlObject);
QVERIFY(oldQmlSingleton != nullptr);
QQmlComponent c(&engine);
c.setData("import ClearSingletons\n"
"import QtQml\n"
"QtObject {\n"
" property QtObject a: CppInstance\n"
#if QT_DEPRECATED_SINCE(6, 3)
" property QtObject f: DeprecatedCppInstance\n"
#endif
" property QtObject b: CppFactory\n"
" property int c: JsValue\n"
" property QtObject d: JsObject\n"
" property QtObject e: QmlSingleton\n"
"}", QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> singletonUser(c.create());
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("a")), &objectCaller1);
#if QT_DEPRECATED_SINCE(6, 3)
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("f")), &objectCaller2);
#endif
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("b")), oldCppSingleton);
QCOMPARE(singletonUser->property("c").toUInt(), 13u);
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("d")), oldJsSingleton);
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("e")), oldQmlSingleton);
engine.clearSingletons();
QCOMPARE(CppSingleton::instantiations, oldCppSingletonId);
QCOMPARE(engine.singletonInstance<ObjectCaller *>(cppInstance), &objectCaller1);
#if QT_DEPRECATED_SINCE(6, 3)
// Singleton instances created with previous versions of qmlRegisterSingletonInstance()
// are lost and cannot be accessed anymore. We can only call their synthesized factory
// functions once. This is not a big problem as you have to recompile in order to use
// clearSingletons() anyway.
QTest::ignoreMessage(
QtWarningMsg,
"<Unknown File>: Singleton registered by registerSingletonInstance "
"must only be accessed from one engine");
QTest::ignoreMessage(
QtCriticalMsg,
"<Unknown File>: qmlRegisterSingletonType(): \"DeprecatedCppInstance\" is not "
"available because the callback function returns a null pointer.");
QCOMPARE(engine.singletonInstance<ObjectCaller *>(deprecatedCppInstance), nullptr);
#endif
QCOMPARE(CppSingleton::instantiations, oldCppSingletonId);
const CppSingleton *newCppSingleton = engine.singletonInstance<CppSingleton *>(cppFactory);
QVERIFY(newCppSingleton != nullptr); // The pointer may be the same as the old one
QCOMPARE(CppSingleton::instantiations, oldCppSingletonId + 1);
QCOMPARE(newCppSingleton->id, CppSingleton::instantiations);
QCOMPARE(engine.singletonInstance<QJSValue>(jsValue).toUInt(), 13u);
const JsSingleton *newJsSingleton = engine.singletonInstance<JsSingleton *>(jsObject);
QVERIFY(newJsSingleton != nullptr);
QVERIFY(newJsSingleton->id != oldJsSingletonId);
const QObject *newQmlSingleton = engine.singletonInstance<QObject *>(qmlObject);
QVERIFY(newQmlSingleton != nullptr);
QVERIFY(newQmlSingleton != oldQmlSingleton);
// Holding on to an old singleton instance is OK. We don't delete those.
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("a")), &objectCaller1);
#if QT_DEPRECATED_SINCE(6, 3)
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("f")), &objectCaller2);
#endif
// Deleting the singletons created by factories has cleared their references in QML.
// We don't renew them as the point of clearing the singletons is not having any
// singletons left afterwards.
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("b")), nullptr);
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("d")), nullptr);
QCOMPARE(qvariant_cast<QObject *>(singletonUser->property("e")), nullptr);
// Value types are unaffected as they are copied.
QCOMPARE(singletonUser->property("c").toUInt(), 13u);
}
void tst_qqmlengine::repeatedCompilation()
{
QQmlEngine engine;
for (int i = 0; i < 100; ++i) {
engine.collectGarbage();
engine.trimComponentCache();
QQmlComponent component(&engine, testFileUrl("repeatedCompilation.qml"));
QVERIFY(component.isReady());
QScopedPointer<QObject> object(component.create());
QVERIFY(object != nullptr);
QCOMPARE(object->property("success").toBool(), true);
}
}
void tst_qqmlengine::failedCompilation()
{
QFETCH(QString, file);
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl(file));
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QQmlComponent: Component is not ready");
QVERIFY(!component.isReady());
QScopedPointer<QObject> object(component.create());
QVERIFY(object.isNull());
engine.collectGarbage();
engine.trimComponentCache();
engine.clearComponentCache();
}
void tst_qqmlengine::failedCompilation_data()
{
QTest::addColumn<QString>("file");
QTest::newRow("Invalid URL") << "failedCompilation.does.not.exist.qml";
QTest::newRow("Invalid content") << "failedCompilation.1.qml";
}
void tst_qqmlengine::outputWarningsToStandardError()
{
QQmlEngine engine;
QCOMPARE(engine.outputWarningsToStandardError(), true);
QQmlComponent c(&engine);
c.setData("import QtQuick 2.0; QtObject { property int a: undefined }", QUrl());
QVERIFY(c.isReady());
QQmlTestMessageHandler messageHandler;
QScopedPointer<QObject> o(c.create());
QVERIFY(o != nullptr);
o.reset();
QCOMPARE(messageHandler.messages().size(), 1);
QCOMPARE(messageHandler.messages().at(0), QLatin1String("<Unknown File>:1:32: Unable to assign [undefined] to int"));
messageHandler.clear();
engine.setOutputWarningsToStandardError(false);
QCOMPARE(engine.outputWarningsToStandardError(), false);
o.reset(c.create());
QVERIFY(o != nullptr);
o.reset();
QVERIFY2(messageHandler.messages().isEmpty(), qPrintable(messageHandler.messageString()));
}
void tst_qqmlengine::objectOwnership()
{
{
QCOMPARE(QQmlEngine::objectOwnership(nullptr), QQmlEngine::CppOwnership);
QQmlEngine::setObjectOwnership(nullptr, QQmlEngine::JavaScriptOwnership);
QCOMPARE(QQmlEngine::objectOwnership(nullptr), QQmlEngine::CppOwnership);
}
{
QObject o;
QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::CppOwnership);
QQmlEngine::setObjectOwnership(&o, QQmlEngine::CppOwnership);
QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::CppOwnership);
QQmlEngine::setObjectOwnership(&o, QQmlEngine::JavaScriptOwnership);
QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::JavaScriptOwnership);
QQmlEngine::setObjectOwnership(&o, QQmlEngine::CppOwnership);
QCOMPARE(QQmlEngine::objectOwnership(&o), QQmlEngine::CppOwnership);
}
{
QQmlEngine engine;
QQmlComponent c(&engine);
c.setData("import QtQuick 2.0; QtObject { property QtObject object: QtObject {} }", QUrl());
QScopedPointer<QObject> o(c.create());
QVERIFY(!o.isNull());
QCOMPARE(QQmlEngine::objectOwnership(o.data()), QQmlEngine::CppOwnership);
QObject *o2 = qvariant_cast<QObject *>(o->property("object"));
QCOMPARE(QQmlEngine::objectOwnership(o2), QQmlEngine::JavaScriptOwnership);
o.reset();
}
{
QObject *ptr = createAQObjectForOwnershipTest();
QSignalSpy spy(ptr, SIGNAL(destroyed()));
{
QQmlEngine engine;
QQmlComponent c(&engine);
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::JavaScriptOwnership);
c.setData("import QtQuick 2.0; Item { required property QtObject test; property int data: test.createAQObjectForOwnershipTest() ? 0 : 1 }", QUrl());
QVERIFY(c.isReady());
QScopedPointer<QObject> o(
c.createWithInitialProperties({{"test", QVariant::fromValue(this)}}));
QVERIFY(o != nullptr);
}
QTRY_VERIFY(spy.size());
}
{
QObject *ptr = new QObject();
QSignalSpy spy(ptr, SIGNAL(destroyed()));
{
QQmlEngine engine;
QQmlComponent c(&engine);
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::JavaScriptOwnership);
c.setData("import QtQuick 2.0; QtObject { required property QtObject test; property var object: { var i = test; test ? 0 : 1 } }", QUrl());
QVERIFY(c.isReady());
QScopedPointer<QObject> o(
c.createWithInitialProperties({{"test", QVariant::fromValue(ptr)}}));
QVERIFY(o != nullptr);
QQmlProperty testProp(o.data(), "test");
testProp.write(QVariant::fromValue<QObject*>(nullptr));
}
QTRY_VERIFY(spy.size());
}
}
// Test an object can be accessed by multiple engines
void tst_qqmlengine::multipleEngines()
{
QObject o;
o.setObjectName("TestName");
// Simultaneous engines
{
QQmlEngine engine1;
QQmlEngine engine2;
engine1.rootContext()->setContextProperty("object", &o);
engine2.rootContext()->setContextProperty("object", &o);
QQmlExpression expr1(engine1.rootContext(), nullptr, QString("object.objectName"));
QQmlExpression expr2(engine2.rootContext(), nullptr, QString("object.objectName"));
QCOMPARE(expr1.evaluate().toString(), QString("TestName"));
QCOMPARE(expr2.evaluate().toString(), QString("TestName"));
}
// Serial engines
{
QQmlEngine engine1;
engine1.rootContext()->setContextProperty("object", &o);
QQmlExpression expr1(engine1.rootContext(), nullptr, QString("object.objectName"));
QCOMPARE(expr1.evaluate().toString(), QString("TestName"));
}
{
QQmlEngine engine1;
engine1.rootContext()->setContextProperty("object", &o);
QQmlExpression expr1(engine1.rootContext(), nullptr, QString("object.objectName"));
QCOMPARE(expr1.evaluate().toString(), QString("TestName"));
}
}
void tst_qqmlengine::qtqmlModule_data()
{
QTest::addColumn<QUrl>("testFile");
QTest::addColumn<QString>("expectedError");
QTest::addColumn<QStringList>("expectedWarnings");
QTest::newRow("import QtQml of correct version (2.0)")
<< testFileUrl("qtqmlModule.1.qml")
<< QString()
<< QStringList();
QTest::newRow("import QtQml of incorrect version (3.0)")
<< testFileUrl("qtqmlModule.2.qml")
<< QString(testFileUrl("qtqmlModule.2.qml").toString() + QLatin1String(":1 module \"QtQml\" version 3.0 is not installed\n"))
<< QStringList();
QTest::newRow("import QtQml of incorrect version (1.0)")
<< testFileUrl("qtqmlModule.3.qml")
<< QString(testFileUrl("qtqmlModule.3.qml").toString() + QLatin1String(":1 module \"QtQml\" version 1.0 is not installed\n"))
<< QStringList();
QTest::newRow("import QtQml of old version (2.50)")
<< testFileUrl("qtqmlModule.4.qml")
<< QString()
<< QStringList();
QTest::newRow("QtQml 2.0 module provides Component, QtObject, Connections, Binding and Timer")
<< testFileUrl("qtqmlModule.5.qml")
<< QString()
<< QStringList();
QTest::newRow("can import QtQml then QtQuick")
<< testFileUrl("qtqmlModule.6.qml")
<< QString()
<< QStringList();
QTest::newRow("can import QtQuick then QtQml")
<< testFileUrl("qtqmlModule.7.qml")
<< QString()
<< QStringList();
QTest::newRow("no import results in no QtObject availability")
<< testFileUrl("qtqmlModule.8.qml")
<< QString(testFileUrl("qtqmlModule.8.qml").toString() + QLatin1String(":4 QtObject is not a type\n"))
<< QStringList();
QTest::newRow("importing QtQml only results in no Item availability")
<< testFileUrl("qtqmlModule.9.qml")
<< QString(testFileUrl("qtqmlModule.9.qml").toString() + QLatin1String(":4 Item is not a type\n"))
<< QStringList();
QTest::newRow("import QtQml of incorrect version (6.50)")
<< testFileUrl("qtqmlModule.10.qml")
<< QString(testFileUrl("qtqmlModule.10.qml").toString() + QLatin1String(":1 module \"QtQml\" version 6.50 is not installed\n"))
<< QStringList();
}
// Test that the engine registers the QtQml module
void tst_qqmlengine::qtqmlModule()
{
QFETCH(QUrl, testFile);
QFETCH(QString, expectedError);
QFETCH(QStringList, expectedWarnings);
for (const QString &w : std::as_const(expectedWarnings))
QTest::ignoreMessage(QtWarningMsg, qPrintable(w));
QQmlEngine e;
QQmlComponent c(&e, testFile);
if (expectedError.isEmpty()) {
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QVERIFY(o);
} else {
QCOMPARE(c.errorString(), expectedError);
}
}
class CustomSelector : public QQmlAbstractUrlInterceptor
{
public:
CustomSelector(const QUrl &base):m_base(base){}
QUrl intercept(const QUrl &url, QQmlAbstractUrlInterceptor::DataType d) override
{
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
QString path = url.path();
if ((url.scheme() != QStringLiteral("file") && url.scheme() != QStringLiteral("qrc"))
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
|| path.contains("QtQml")) {
return url;
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
}
if (!m_interceptionPoints.contains(d))
return url;
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
if (path.endsWith("Test.2/qmldir")) {
// Special case
QUrl url = m_base;
url.setPath(m_base.path() + "interception/module/intercepted/qmldir");
return url;
}
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
qsizetype lastSlash = path.lastIndexOf('/');
if (lastSlash < 0)
lastSlash = 0;
if (QStringView(path).mid(lastSlash) == u"/included.js") {
// Special case: We want this one to be double-intercepted
} else if (QStringView(path).left(lastSlash).endsWith(u"/intercepted")) {
// Already intercepted. Don't do it again.
// In real applications this routinely happens when new URLs are constructed relative to
// components loaded from intercepted URLs. Any sensible URL interceptor has to filter
// for this.
return url;
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
}
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
path.insert(lastSlash, QStringLiteral("/intercepted"));
QUrl ret = url;
QtQml: Do not store type references for properties This allows a type A to have a property of type B while B also has a property of type A. This is very helpful for list-like or otherwise linked data structures. For example the BaseConstraint type in the DeltaBlue benchmark wouldn't have been necessary if this feature had been available before. It has the side effect of making it impossible to validate inline component types before they're instantiated. We don't actually have the type at the point where a property is declared. However, since you certainly want to instantiate at some point and qmllint will still help you find the problem, this is acceptable. We also have to intercept URLs before we create QQmlType instances now. This is because now we only have the URLs to identify types. Having multiple QQmlType instances referring to the same logical type wsd a bad idea anyway. [ChangeLog][QtQml] You can now have cyclic type references in QML documents. A QML type A can have a property of type B while B has a property of type A. You can still not cyclically inherit or instantiate types in QML documents, of course [ChangeLog][QtQml] Your URL interceptors may get called more often now, for URLs QML neglected to intercept before. This means that more URLs may be constructed from already-intercepted base URLs. URL interceptors should check for this condition. Change-Id: I84753a883c2de98e16d1c3826e7e56e897eeafb8 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
2025-05-12 14:31:06 +00:00
ret.setPath(path);
return ret;
}
QList<QQmlAbstractUrlInterceptor::DataType> m_interceptionPoints;
QUrl m_base;
};
Q_DECLARE_METATYPE(QList<QQmlAbstractUrlInterceptor::DataType>);
void tst_qqmlengine::urlInterceptor_data()
{
QTest::addColumn<QUrl>("testFile");
QTest::addColumn<QList<QQmlAbstractUrlInterceptor::DataType> >("interceptionPoint");
QTest::addColumn<QString>("expectedChildString");
QTest::addColumn<QString>("expectedScriptString");
QTest::addColumn<QString>("expectedResolvedUrl");
QTest::addColumn<QString>("expectedAbsoluteUrl");
QTest::newRow("InterceptTypes")
<< testFileUrl("interception/types/urlInterceptor.qml")
<< (QList<QQmlAbstractUrlInterceptor::DataType>() << QQmlAbstractUrlInterceptor::QmlFile << QQmlAbstractUrlInterceptor::JavaScriptFile << QQmlAbstractUrlInterceptor::UrlString)
<< QStringLiteral("intercepted")
<< QStringLiteral("intercepted")
<< testFileUrl("interception/types/intercepted/doesNotExist.file").toString()
<< QStringLiteral("file:///intercepted/doesNotExist.file");
QTest::newRow("InterceptQmlDir")
<< testFileUrl("interception/qmldir/urlInterceptor.qml")
<< (QList<QQmlAbstractUrlInterceptor::DataType>() << QQmlAbstractUrlInterceptor::QmldirFile << QQmlAbstractUrlInterceptor::UrlString)
<< QStringLiteral("intercepted")
<< QStringLiteral("base file")
<< testFileUrl("interception/qmldir/intercepted/doesNotExist.file").toString()
<< QStringLiteral("file:///intercepted/doesNotExist.file");
QTest::newRow("InterceptModule")//just a Test{}, needs to intercept the module import for it to work
<< testFileUrl("interception/module/urlInterceptor.qml")
<< (QList<QQmlAbstractUrlInterceptor::DataType>() << QQmlAbstractUrlInterceptor::QmldirFile )
<< QStringLiteral("intercepted")
<< QStringLiteral("intercepted")
<< testFileUrl("interception/module/intercepted/doesNotExist.file").toString()
<< QStringLiteral("file:///doesNotExist.file");
QTest::newRow("InterceptStrings")
<< testFileUrl("interception/strings/urlInterceptor.qml")
<< (QList<QQmlAbstractUrlInterceptor::DataType>() << QQmlAbstractUrlInterceptor::UrlString)
<< QStringLiteral("base file")
<< QStringLiteral("base file")
<< testFileUrl("interception/strings/intercepted/doesNotExist.file").toString()
<< QStringLiteral("file:///intercepted/doesNotExist.file");
QTest::newRow("InterceptIncludes")
<< testFileUrl("interception/includes/urlInterceptor.qml")
<< (QList<QQmlAbstractUrlInterceptor::DataType>() << QQmlAbstractUrlInterceptor::JavaScriptFile)
<< QStringLiteral("base file")
<< QStringLiteral("intercepted include file")
<< testFileUrl("interception/includes/doesNotExist.file").toString()
<< QStringLiteral("file:///doesNotExist.file");
}
void tst_qqmlengine::urlInterceptor()
{
QFETCH(QUrl, testFile);
QFETCH(QList<QQmlAbstractUrlInterceptor::DataType>, interceptionPoint);
QFETCH(QString, expectedChildString);
QFETCH(QString, expectedScriptString);
QFETCH(QString, expectedResolvedUrl);
QFETCH(QString, expectedAbsoluteUrl);
QQmlEngine e;
e.addImportPath(testFileUrl("interception/imports").url());
CustomSelector cs(testFileUrl(""));
cs.m_interceptionPoints = interceptionPoint;
e.addUrlInterceptor(&cs);
QQmlComponent c(&e, testFile); //Note that this can get intercepted too
QScopedPointer<QObject> o(c.create());
if (!o)
qDebug() << c.errorString();
QVERIFY(o);
//Test a URL as a property initialization
QCOMPARE(o->property("filePath").toString(), QUrl("doesNotExist.file").toString());
//Test a URL as a Type location
QCOMPARE(o->property("childString").toString(), expectedChildString);
//Test a URL as a Script location
QCOMPARE(o->property("scriptString").toString(), expectedScriptString);
//Test a URL as a resolveUrl() call
QCOMPARE(o->property("resolvedUrl").toString(), expectedResolvedUrl);
QCOMPARE(o->property("absoluteUrl").toString(), expectedAbsoluteUrl);
}
void tst_qqmlengine::qmlContextProperties()
{
QQmlEngine e;
QQmlComponent c(&e, testFileUrl("TypeofQmlProperty.qml"));
QScopedPointer<QObject> o(c.create());
if (!o) {
qDebug() << c.errorString();
}
QVERIFY(o);
}
void tst_qqmlengine::testGCCorruption()
{
QQmlEngine e;
QQmlComponent c(&e, testFileUrl("testGCCorruption.qml"));
QScopedPointer<QObject> o(c.create());
QVERIFY2(o, qPrintable(c.errorString()));
}
void tst_qqmlengine::testGroupedPropertyRevisions()
{
QQmlEngine e;
QQmlComponent c(&e, testFileUrl("testGroupedPropertiesRevision.1.qml"));
QScopedPointer<QObject> object(c.create());
QVERIFY2(object.data(), qPrintable(c.errorString()));
QQmlComponent c2(&e, testFileUrl("testGroupedPropertiesRevision.2.qml"));
QVERIFY(!c2.errorString().isEmpty());
}
void tst_qqmlengine::componentFromEval()
{
QQmlEngine engine;
const QUrl testUrl = testFileUrl("EmptyComponent.qml");
QJSValue result = engine.evaluate("Qt.createComponent(\"" + testUrl.toString() + "\");");
QPointer<QQmlComponent> component(qobject_cast<QQmlComponent*>(result.toQObject()));
QVERIFY(!component.isNull());
QScopedPointer<QObject> item(component->create());
QVERIFY(!item.isNull());
}
void tst_qqmlengine::qrcUrls()
{
QQmlEngine engine;
QQmlTypeLoader *typeLoader = QQmlTypeLoader::get(&engine);
{
QQmlRefPointer<QQmlTypeData> oneQml(typeLoader->getType(QUrl("qrc:/qrcurls.qml")));
QVERIFY(oneQml.data() != nullptr);
QVERIFY(!oneQml->backupSourceCode().isValid());
QQmlRefPointer<QQmlTypeData> twoQml(typeLoader->getType(QUrl("qrc:///qrcurls.qml")));
QVERIFY(twoQml.data() != nullptr);
QCOMPARE(oneQml.data(), twoQml.data());
}
{
QQmlRefPointer<QQmlTypeData> oneJS(typeLoader->getType(QUrl("qrc:/qrcurls.js")));
QVERIFY(oneJS.data() != nullptr);
QVERIFY(!oneJS->backupSourceCode().isValid());
QQmlRefPointer<QQmlTypeData> twoJS(typeLoader->getType(QUrl("qrc:///qrcurls.js")));
QVERIFY(twoJS.data() != nullptr);
QCOMPARE(oneJS.data(), twoJS.data());
}
}
void tst_qqmlengine::cppSignalAndEval()
{
ObjectCaller objectCaller;
QQmlEngine engine;
qmlRegisterSingletonInstance("Test", 1, 0, "CallerCpp", &objectCaller);
QQmlComponent c(&engine);
c.setData("import QtQuick 2.9\n"
"import Test 1.0\n"
"Item {\n"
" property var r: 0\n"
" Connections {\n"
" target: CallerCpp;\n"
" function onDoubleReply() {\n"
" eval('var z = 1');\n"
" r = a;\n"
" }\n"
" }\n"
"}",
QUrl(QStringLiteral("qrc:/main.qml")));
QScopedPointer<QObject> object(c.create());
QVERIFY(!object.isNull());
emit objectCaller.doubleReply(1.1234);
QCOMPARE(object->property("r"), 1.1234);
}
class SomeQObjectClass : public QObject {
Q_OBJECT
public:
SomeQObjectClass() : QObject(nullptr){}
};
class Dayfly : public QObject
{
Q_OBJECT
};
void tst_qqmlengine::singletonInstance()
{
QQmlEngine engine;
int cppSingletonTypeId = qmlRegisterSingletonType<CppSingleton>("Test", 1, 0, "CppSingleton", &CppSingleton::create);
int jsValueSingletonTypeId = qmlRegisterSingletonType("Test", 1, 0, "JsSingleton", &JsSingleton::create);
{
// Cpp QObject singleton type
QJSValue value = engine.singletonInstance<QJSValue>(cppSingletonTypeId);
QVERIFY(!value.isUndefined());
QVERIFY(value.isQObject());
QObject *instance = value.toQObject();
QVERIFY(instance);
QCOMPARE(instance->metaObject()->className(), "CppSingleton");
QCOMPARE(engine.singletonInstance<CppSingleton *>("Test", "CppSingleton"), instance);
}
{
// QJSValue QObject singleton type
QJSValue value = engine.singletonInstance<QJSValue>(jsValueSingletonTypeId);
QVERIFY(!value.isUndefined());
QVERIFY(value.isQObject());
QObject *instance = value.toQObject();
QVERIFY(instance);
QCOMPARE(instance->metaObject()->className(), "JsSingleton");
}
{
int data = 30;
auto id = qmlRegisterSingletonType<CppSingleton>("Qt.test",1,0,"CapturingLambda",[data](QQmlEngine*, QJSEngine*){ // register qobject singleton with capturing lambda
auto o = new CppSingleton;
o->setProperty("data", data);
return o;
});
QJSValue value = engine.singletonInstance<QJSValue>(id);
QVERIFY(!value.isUndefined());
QVERIFY(value.isQObject());
QObject *instance = value.toQObject();
QVERIFY(instance);
QCOMPARE(instance->metaObject()->className(), "CppSingleton");
QCOMPARE(instance->property("data"), data);
}
{
qmlRegisterSingletonType<CppSingleton>("Qt.test",1,0,"NotAmbiguous", [](QQmlEngine* qeng, QJSEngine* jeng) -> QObject* {return CppSingleton::create(qeng, jeng);}); // test that overloads for qmlRegisterSingleton are not ambiguous
}
{
// Register QObject* directly
CppSingleton single;
int id = qmlRegisterSingletonInstance("Qt.test", 1, 0, "CppOwned",
&single);
QQmlEngine engine2;
CppSingleton *singlePtr = engine2.singletonInstance<CppSingleton *>(id);
QVERIFY(singlePtr);
QCOMPARE(&single, singlePtr);
QVERIFY(engine2.objectOwnership(singlePtr) == QQmlEngine::CppOwnership);
}
{
CppSingleton single;
QQmlEngine engineA;
QQmlEngine engineB;
int id = qmlRegisterSingletonInstance("Qt.test", 1, 0, "CppOwned", &single);
auto singlePtr = engineA.singletonInstance<CppSingleton *>(id);
QVERIFY(singlePtr);
singlePtr = engineA.singletonInstance<CppSingleton *>(id); // accessing the singleton multiple times from the same engine is fine
QVERIFY(singlePtr);
QTest::ignoreMessage(QtMsgType::QtCriticalMsg, "<Unknown File>: qmlRegisterSingletonType(): \"CppOwned\" is not available because the callback function returns a null pointer.");
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "<Unknown File>: Singleton registered by registerSingletonInstance must only be accessed from one engine");
QCOMPARE(&single, singlePtr);
auto noSinglePtr = engineB.singletonInstance<CppSingleton *>(id);
QVERIFY(!noSinglePtr);
}
{
CppSingleton single;
QThread newThread {};
single.moveToThread(&newThread);
QCOMPARE(single.thread(), &newThread);
QQmlEngine engineB;
int id = qmlRegisterSingletonInstance("Qt.test", 1, 0, "CppOwned", &single);
QTest::ignoreMessage(QtMsgType::QtCriticalMsg, "<Unknown File>: qmlRegisterSingletonType(): \"CppOwned\" is not available because the callback function returns a null pointer.");
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "<Unknown File>: Registered object must live in the same thread as the engine it was registered with");
auto noSinglePtr = engineB.singletonInstance<CppSingleton *>(id);
QVERIFY(!noSinglePtr);
}
// test the case where we haven't loaded the module yet
{
auto singleton = engine.singletonInstance<PurelyDeclarativeSingleton *>("OnlyDeclarative", "PurelyDeclarativeSingleton");
QVERIFY(singleton);
// requesting the singleton twice yields the same result
auto again = engine.singletonInstance<PurelyDeclarativeSingleton *>("OnlyDeclarative", "PurelyDeclarativeSingleton");
QCOMPARE(again, singleton);
// different engines -> different singletons
QQmlEngine engine2;
auto differentEngine = engine2.singletonInstance<PurelyDeclarativeSingleton *>("OnlyDeclarative", "PurelyDeclarativeSingleton");
QCOMPARE_NE(differentEngine, singleton);
}
{
// Invalid types
QJSValue value;
value = engine.singletonInstance<QJSValue>(-4711);
QVERIFY(value.isUndefined());
value = engine.singletonInstance<QJSValue>(1701);
QVERIFY(value.isUndefined());
}
{
// Valid, but non-singleton type
int typeId = qmlRegisterType<CppSingleton>("Test", 1, 0, "NotASingleton");
QJSValue value = engine.singletonInstance<QJSValue>(typeId);
QVERIFY(value.isUndefined());
}
{
// Cpp QObject singleton type
CppSingleton *instance = engine.singletonInstance<CppSingleton*>(cppSingletonTypeId);
QVERIFY(instance);
QCOMPARE(instance->metaObject()->className(), "CppSingleton");
QCOMPARE(instance, engine.singletonInstance<QJSValue>(cppSingletonTypeId).toQObject());
}
{
// Wrong destination type
SomeQObjectClass * instance = engine.singletonInstance<SomeQObjectClass*>(cppSingletonTypeId);
QVERIFY(!instance);
}
{
// deleted object
auto dayfly = std::make_unique<Dayfly>();
auto id = qmlRegisterSingletonInstance("Vanity", 1, 0, "Dayfly", dayfly.get());
dayfly.reset();
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "<Unknown File>: The registered singleton has already been deleted. Ensure that it outlives the engine.");
QObject *instance = engine.singletonInstance<QObject*>(id);
QVERIFY(!instance);
}
}
void tst_qqmlengine::aggressiveGc()
{
const QByteArray origAggressiveGc = qgetenv("QV4_MM_AGGRESSIVE_GC");
qputenv("QV4_MM_AGGRESSIVE_GC", "true");
{
QQmlEngine engine; // freezing should not run into infinite recursion
QJSValue obj = engine.newObject();
QVERIFY(obj.isObject());
}
qputenv("QV4_MM_AGGRESSIVE_GC", origAggressiveGc);
}
void tst_qqmlengine::cachedGetterLookup_qtbug_75335()
{
QQmlEngine engine;
const QUrl testUrl = testFileUrl("CachedGetterLookup.qml");
QQmlComponent component(&engine, testUrl);
QVERIFY(component.isReady());
QScopedPointer<QObject> object(component.create());
QVERIFY(object != nullptr);
}
class EvilSingleton : public QObject
{
Q_OBJECT
public:
QPointer<QQmlEngine> m_engine;
EvilSingleton(QQmlEngine *engine) : m_engine(engine) {
connect(this, &QObject::destroyed, this, [this]() {
QQmlComponent component(m_engine);
component.setData("import QtQml 2.0\nQtObject {}", QUrl("file://Stuff.qml"));
QVERIFY(component.isReady());
QScopedPointer<QObject> obj(component.create());
QVERIFY(obj);
});
}
};
void tst_qqmlengine::createComponentOnSingletonDestruction()
{
qmlRegisterSingletonType<EvilSingleton>("foo.foo", 1, 0, "Singleton",
[](QQmlEngine *engine, QJSEngine *) {
return new EvilSingleton(engine);
});
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("evilSingletonInstantiation.qml"));
QVERIFY(component.isReady());
QScopedPointer<QObject> obj(component.create());
QVERIFY(obj);
}
void tst_qqmlengine::uiLanguage()
{
const QRegularExpression bindingLoopWarningRegex(".*QML QtObject: Binding loop detected for property \"textToTranslate\".*");
{
QQmlEngine engine;
QObject::connect(&engine, &QJSEngine::uiLanguageChanged, [&engine]() {
engine.retranslate();
});
QSignalSpy uiLanguageChangeSpy(&engine, SIGNAL(uiLanguageChanged()));
QQmlComponent component(&engine, testFileUrl("uiLanguage.qml"));
QTest::ignoreMessage(QtMsgType::QtWarningMsg, bindingLoopWarningRegex);
QScopedPointer<QObject> object(component.create());
QVERIFY(!object.isNull());
QVERIFY(engine.uiLanguage().isEmpty());
QCOMPARE(object->property("numberOfTranslationBindingEvaluations").toInt(), 1);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, bindingLoopWarningRegex);
engine.setUiLanguage("TestLanguage");
QCOMPARE(object->property("numberOfTranslationBindingEvaluations").toInt(), 2);
QCOMPARE(object->property("chosenLanguage").toString(), "TestLanguage");
QTest::ignoreMessage(QtMsgType::QtWarningMsg, bindingLoopWarningRegex);
engine.evaluate("Qt.uiLanguage = \"anotherLanguage\"");
QCOMPARE(engine.uiLanguage(), QString("anotherLanguage"));
QCOMPARE(object->property("numberOfTranslationBindingEvaluations").toInt(), 3);
QCOMPARE(object->property("chosenLanguage").toString(), "anotherLanguage");
}
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("uiLanguage.qml"));
QTest::ignoreMessage(QtMsgType::QtWarningMsg, bindingLoopWarningRegex);
QScopedPointer<QObject> object(component.create());
QVERIFY(!object.isNull());
engine.setUiLanguage("TestLanguage");
QCOMPARE(object->property("chosenLanguage").toString(), "TestLanguage");
}
}
class I18nAwareClass : public QObject {
Q_OBJECT
QML_NAMED_ELEMENT(I18nAware)
Q_PROPERTY(QString text READ text NOTIFY textChanged)
signals:
void textChanged();
public:
int counter = 0;
QString text()
{
if (auto engine = qmlEngine(this))
engine->markCurrentFunctionAsTranslationBinding();
return QLatin1String("Hello, %1").arg(QString::number(counter++));
}
};
void tst_qqmlengine::markCurrentFunctionAsTranslationBinding()
{
QQmlEngine engine;
qmlRegisterTypesAndRevisions<I18nAwareClass>("i18ntest", 1);
QQmlComponent comp(&engine, testFileUrl("markCurrentFunctionAsTranslationBinding.qml"));
std::unique_ptr<QObject> root { comp.create() };
QCOMPARE(root->property("result"), "Hello, 0");
engine.retranslate();
QCOMPARE(root->property("result"), "Hello, 1");
}
void tst_qqmlengine::executeRuntimeFunction()
{
QQmlEngine engine;
QQmlEnginePrivate *priv = QQmlEnginePrivate::get(std::addressof(engine));
const QUrl url = testFileUrl("runtimeFunctions.qml");
QQmlComponent component(&engine, url);
QVERIFY2(component.isReady(), qPrintable(component.errorString()));
QScopedPointer<QObject> dummy(component.create());
QVERIFY(dummy);
// getConstantValue():
int constant = 0;
void *a0[] = { const_cast<void *>(reinterpret_cast<const void *>(std::addressof(constant))) };
QMetaType t0[] = { QMetaType::fromType<int>() };
priv->executeRuntimeFunction(url, /* index = */ 0, dummy.get(), /* argc = */ 0, a0, t0);
QCOMPARE(constant, 42);
// squareValue():
int squared = 0;
int x = 5;
void *a1[] = { const_cast<void *>(reinterpret_cast<const void *>(std::addressof(squared))),
const_cast<void *>(reinterpret_cast<const void *>(std::addressof(x))) };
QMetaType t1[] = { QMetaType::fromType<int>(), QMetaType::fromType<int>() };
priv->executeRuntimeFunction(url, /* index = */ 1, dummy.get(), /* argc = */ 1, a1, t1);
QCOMPARE(squared, x * x);
// concatenate():
QString concatenated;
QString str1 = QStringLiteral("Hello"); // uses "raw data" storage
QString str2 = QLatin1String(", Qml"); // uses own QString storage
void *a2[] = { const_cast<void *>(reinterpret_cast<const void *>(std::addressof(concatenated))),
const_cast<void *>(reinterpret_cast<const void *>(std::addressof(str1))),
const_cast<void *>(reinterpret_cast<const void *>(std::addressof(str2))) };
QMetaType t2[] = { QMetaType::fromType<QString>(), QMetaType::fromType<QString>(),
QMetaType::fromType<QString>() };
priv->executeRuntimeFunction(url, /* index = */ 2, dummy.get(), /* argc = */ 2, a2, t2);
QCOMPARE(concatenated, str1 + str2);
// capture `this`:
QCOMPARE(dummy->property("foo").toInt(), 42);
QCOMPARE(dummy->property("bar").toInt(), 0);
priv->executeRuntimeFunction(url, /* index = */ 4, dummy.get());
QCOMPARE(dummy->property("bar").toInt(), 1 + 42 + 1);
QCOMPARE(dummy->property("baz").toInt(), -100);
int y = 1;
void *a3[] = { nullptr, const_cast<void *>(reinterpret_cast<const void *>(&y)) };
QMetaType t3[] = { QMetaType::fromType<void>(), QMetaType::fromType<int>() };
priv->executeRuntimeFunction(url, /* index = */ 6, dummy.get(), 1, a3, t3);
QCOMPARE(dummy->property("bar").toInt(), -98);
QCOMPARE(dummy->property("baz").toInt(), -100);
}
class WithQProperty : public QObject
{
Q_OBJECT
Q_PROPERTY(int foo READ foo WRITE setFoo BINDABLE fooBindable)
public:
WithQProperty(QObject *parent = nullptr) : QObject(parent) { m_foo.setValue(12); }
int foo() const { return m_foo.value(); }
void setFoo(int foo) { m_foo.setValue(foo); }
QBindable<int> fooBindable() { return QBindable<int>(&m_foo); }
int getFooWithCapture()
{
const QMetaObject *m = metaObject();
currentEngine->captureProperty(this, m->property(m->indexOfProperty("foo")));
return m_foo.value();
}
static QQmlEngine *currentEngine;
private:
QProperty<int> m_foo;
};
class WithoutQProperty : public QObject
{
Q_OBJECT
Q_PROPERTY(int foo READ foo WRITE setFoo NOTIFY fooChanged)
public:
WithoutQProperty(QObject *parent = nullptr) : QObject(parent), m_foo(new WithQProperty(this)) {}
int foo() const { return m_foo->getFooWithCapture(); }
void setFoo(int foo) {
if (foo != m_foo->foo()) {
m_foo->setFoo(foo);
emit fooChanged();
}
}
void triggerBinding(int val)
{
m_foo->setFoo(val);
}
signals:
void fooChanged();
private:
WithQProperty *m_foo;
};
QQmlEngine *WithQProperty::currentEngine = nullptr;
void tst_qqmlengine::captureQProperty()
{
qmlRegisterType<WithoutQProperty>("Foo", 1, 0, "WithoutQProperty");
QQmlEngine engine;
WithQProperty::currentEngine = &engine;
QQmlComponent c(&engine);
c.setData("import Foo\n"
"WithoutQProperty {\n"
" property int x: foo\n"
"}", QUrl());
QVERIFY2(c.isReady(), c.errorString().toUtf8());
QScopedPointer<QObject> o(c.create());
QCOMPARE(o->property("x").toInt(), 12);
static_cast<WithoutQProperty *>(o.data())->triggerBinding(13);
QCOMPARE(o->property("x").toInt(), 13);
}
void tst_qqmlengine::listWrapperAsListReference()
{
QQmlEngine engine;
QQmlComponent c(&engine);
c.setData("import QtQml\nQtObject {\nproperty list<QtObject> c: [ QtObject {} ]\n}", QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QJSManagedValue m = engine.toManagedValue(o.data());
QJSValue prop = m.property("c");
const QQmlListReference ref = qjsvalue_cast<QQmlListReference>(prop);
QCOMPARE(ref.size(), 1);
}
void tst_qqmlengine::attachedObjectAsObject()
{
QQmlEngine engine;
QQmlComponent c(&engine);
c.setData("import QtQml\nQtObject { property var a: Component }", QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QJSManagedValue m = engine.toManagedValue(o.data());
QJSValue prop = m.property("a");
const QQmlComponentAttached *attached = qjsvalue_cast<QQmlComponentAttached *>(prop);
QCOMPARE(attached, qmlAttachedPropertiesObject<QQmlComponent>(o.data()));
}
class WithListProperty : public QObject
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<QQmlComponent> components READ components CONSTANT)
QML_ELEMENT
public:
QQmlListProperty<QQmlComponent> components()
{
return QQmlListProperty<QQmlComponent>(this, &m_components);
}
private:
QList<QQmlComponent *> m_components;
};
void tst_qqmlengine::listPropertyAsQJSValue()
{
qmlRegisterTypesAndRevisions<WithListProperty>("Foo", 1);
QQmlEngine engine;
QQmlComponent c(&engine);
c.setData("import Foo\nWithListProperty {}", QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
WithListProperty *parent = qobject_cast<WithListProperty *>(o.data());
QVERIFY(parent);
QQmlListProperty<QQmlComponent> prop = parent->components();
QJSValue val = engine.toScriptValue(prop);
QQmlListReference ref = engine.fromScriptValue<QQmlListReference>(val);
ref.append(&c);
QCOMPARE(prop.count(&prop), 1);
QCOMPARE(prop.at(&prop, 0), &c);
}
void tst_qqmlengine::stringToColor()
{
QQmlEngine engine;
// Make it import QtQuick, so that color becomes available.
QQmlComponent c(&engine);
c.setData("import QtQuick\nItem {}", QUrl());
QVERIFY(c.isReady());
QScopedPointer<QObject> o(c.create());
const QMetaType metaType(QMetaType::QColor);
QVariant color(metaType);
QV4::Scope scope(engine.handle());
QV4::ScopedValue colorString(scope, engine.handle()->newString(QStringLiteral("#abcdef")));
QVERIFY(QV4::ExecutionEngine::metaTypeFromJS(
colorString,
metaType, color.data()));
QVERIFY(color.isValid());
QCOMPARE(color.metaType(), metaType);
QVariant variant(QStringLiteral("#abcdef"));
QVERIFY(variant.convert(metaType));
QCOMPARE(variant.metaType(), metaType);
QCOMPARE(color, variant);
}
class WithToString : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
Q_INVOKABLE QString toString() const { return QStringLiteral("things"); }
};
class WithToNumber : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
Q_INVOKABLE int toString() const { return 4; }
};
void tst_qqmlengine::qobjectToString()
{
qmlRegisterTypesAndRevisions<WithToString>("WithToString", 1);
qmlRegisterTypesAndRevisions<WithToNumber>("WithToString", 1);
QQmlEngine engine;
QQmlComponent c(&engine);
c.setData(R"(
import WithToString
import QtQml
WithToString {
id: self
property QtObject weird: WithToNumber {}
objectName: toString() + ' ' + self.toString() + ' ' + weird.toString()
}
)", QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QCOMPARE(o->objectName(), QStringLiteral("things things 4"));
}
void tst_qqmlengine::qtNamespaceInQtObject()
{
QQmlEngine engine;
QJSValue qtObject = engine.globalObject().property(QStringLiteral("Qt"));
// Qt namespace enums are there.
QCOMPARE(qtObject.property(QStringLiteral("Checked")).toInt(), 2);
// QtObject methods are also there.
QVERIFY(qtObject.property(QStringLiteral("rect")).isCallable());
// QObject is also there.
QVERIFY(qtObject.hasProperty(QStringLiteral("objectName")));
}
void tst_qqmlengine::nativeModuleImport()
{
QQmlEngine engine;
QJSValue name("TheName");
QJSValue obj = engine.newObject();
obj.setProperty("name", name);
engine.registerModule("info.mjs", obj);
QQmlComponent c(&engine, testFileUrl("nativeModuleImport.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QCOMPARE(o->property("a").toString(), QStringLiteral("Hello World"));
QCOMPARE(o->property("b").toString(), QStringLiteral("TheName"));
QCOMPARE(o->property("c").toString(), QStringLiteral("TheName"));
QCOMPARE(o->property("d").toString(), QStringLiteral("TheName"));
QCOMPARE(o->property("e").toString(), QStringLiteral("TheName"));
}
Allow limited extensions to globals We can allow a, overriding data members of globals, such as Error.name b, adding members that don't clash with any internals c, any manipulation of toString(), toLocaleString(), valueOf(), and constructor To that effect, add a "Locked" flag to our internal classes. If that is set, disallow changing prototypes and when defining a property, check if it shadows any non-configurable property. Furthermore, make all non-primitive properties that are not meant to be overridden non-configurable and non-writable. constructor, toString(), toLocaleString() and valueOf() are exempt because they are explicitly meant to be overridden by users. Therefore, we let that happen and refrain from optimizing them or triggering their implicit invocation in optimized code. [ChangeLog][QtQml][Important Behavior Changes] The JavaScript global objects are not frozen anymore in a QML engine. Instead, they are selectively locked. You can extend the objects with new members as long as you don't shadow any existing methods, and you can change or override data members. This also means that most methods of Object.prototype, which was previously exempt from the freezing, cannot be changed anymore. You can, however, change or override constructor, toString(), toLocaleString() and valueOf() on any prototype. Those are clearly meant to be overridden by user code. Fixes: QTBUG-101298 Task-number: QTBUG-84341 Change-Id: Id77db971f76c8f48b18e7a93607da5f947ecfc3e Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
2022-08-29 10:00:52 +00:00
void tst_qqmlengine::lockedRootObject()
{
QQmlEngine engine;
QQmlComponent c(&engine, testFileUrl("lockedRootObject.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QTest::ignoreMessage(
QtWarningMsg, "You cannot shadow the locked property 'hasOwnProperty' in QML.");
QScopedPointer<QObject> o(c.create());
QCOMPARE(o->property("myErrorName").toString(), QStringLiteral("MyError1"));
QCOMPARE(o->property("errorName").toString(), QStringLiteral("MyError2"));
QCOMPARE(o->property("mathMax").toInt(), 4);
QCOMPARE(o->property("extendGlobal").toInt(), 32);
QCOMPARE(o->property("prototypeTrick").toString(), QStringLiteral("SyntaxError"));
QCOMPARE(o->property("shadowMethod1").toString(), QStringLiteral("not a TypeError"));
QCOMPARE(o->property("shadowMethod2").toBool(), false);
QCOMPARE(o->property("changeObjectProto1").toString(), QStringLiteral("not an Object"));
QCOMPARE(o->property("changeObjectProto2").toBool(), false);
QCOMPARE(o->property("defineProperty1").toString(), QStringLiteral("not a URIError"));
QCOMPARE(o->property("defineProperty2").toBool(), false);
}
void tst_qqmlengine::crossReferencingSingletonsDeletion()
{
QQmlEngine engine;
engine.addImportPath(testFileUrl("crossReferencingSingletonsDeletion").url());
QQmlComponent c(&engine, testFileUrl("crossReferencingSingletonsDeletion/Module/Main.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
std::unique_ptr<QObject> o{ c.create() };
QVERIFY(o);
QCOMPARE(o->property("s").toString(), "SingletonA");
}
void tst_qqmlengine::bindingInstallUseAfterFree()
{
QQmlEngine engine;
QQmlComponent c(&engine, testFileUrl("bindingInstallUseAfterFree.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
std::unique_ptr<QObject> o{ c.create() };
QVERIFY(o);
}
void tst_qqmlengine::objectListArgumentMethod()
{
QQmlEngine engine;
QQmlComponent c(&engine, testFileUrl("objectListArgumentMethod.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QTest::ignoreMessage(QtMsgType::QtDebugMsg, "5");
std::unique_ptr<QObject> o{ c.create() };
QVERIFY(o);
}
void tst_qqmlengine::variantListQJsonConversion()
{
QQmlEngine engine;
QQmlComponent c(&engine, testFileUrl("variantListQJsonConversion.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QTest::ignoreMessage(QtMsgType::QtDebugMsg, R"(["cpp","variant","list"])");
QTest::ignoreMessage(QtMsgType::QtDebugMsg, R"({"test":["cpp","variant","list"]})");
QTest::ignoreMessage(QtMsgType::QtDebugMsg,
R"([{"objectName":"o0"},{"objectName":"o1"},{"objectName":"o2"}])");
QScopedPointer<QObject> o(c.create());
QVERIFY(o);
}
class UnregisteredAttached : public QObject
{
Q_OBJECT
public:
UnregisteredAttached(QObject *parent = nullptr) : QObject(parent) {}
};
class Unregistered : public QObject
{
Q_OBJECT
QML_ATTACHED(UnregisteredAttached)
public:
static UnregisteredAttached *qmlAttachedProperties(QObject *obj)
{
return new UnregisteredAttached(obj);
}
};
void tst_qqmlengine::attachedObjectOfUnregistered()
{
QObject o;
QObject *a = qmlAttachedPropertiesObject<Unregistered>(&o);
QVERIFY(a);
QVERIFY(qobject_cast<UnregisteredAttached *>(a));
QObject *b = qmlAttachedPropertiesObject<Unregistered>(&o);
QCOMPARE(a, b);
QObject o2;
QObject *c = qmlAttachedPropertiesObject<Unregistered>(&o2);
QVERIFY(c);
QVERIFY(qobject_cast<UnregisteredAttached *>(c));
QVERIFY(c != a);
}
void tst_qqmlengine::dropCUOnEngineShutdown()
{
QScopedPointer<QObject> o;
const QUrl url("qrc:/some/Type.qml");
{
QQmlEngine e;
QQmlComponent c(&e);
c.setData("import QtQml\nQtObject {}", url);
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
o.reset(c.create());
QVERIFY(!o.isNull());
QVERIFY(QByteArray(o->metaObject()->className()).startsWith("Type_QMLTYPE_"));
QVERIFY(QQmlMetaType::qmlType(url).isValid());
QVERIFY(!QQmlMetaType::obtainCompilationUnit(url).isNull());
}
// The object is still there (don't do this at home).
QVERIFY(!o.isNull());
// QMetaObject is still ok.
QVERIFY(QByteArray(o->metaObject()->className()).startsWith("Type_QMLTYPE_"));
// But the QQmlType is gone.
QVERIFY(!QQmlMetaType::qmlType(url).isValid());
// And also the CU.
QVERIFY(QQmlMetaType::obtainCompilationUnit(url).isNull());
}
void tst_qqmlengine::multiLoadedJavaScriptModule()
{
QQmlEngine e;
QQmlComponent c(&e, testFileUrl("multiLoaded.qml"));
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QTest::ignoreMessage(QtCriticalMsg, "In a1 - DATA_IN_A1");
QTest::ignoreMessage(QtCriticalMsg, "In B1 DATA_IN_A1");
QTest::ignoreMessage(QtCriticalMsg, "In B1 DATA_IN_A1");
QScopedPointer<QObject> o(c.create());
QVERIFY(!o.isNull());
}
void tst_qqmlengine::metaObjectOfScriptCU()
{
QQmlEngine engine;
engine.addImportPath(dataDirectory());
QQmlComponent c(&engine);
c.setData(R"(
import JS
import QtQml
QtObject {
objectName: ColorUtils.withOpacity('blue', 0.5)
})", QUrl());
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
QScopedPointer<QObject> o(c.create());
QVERIFY(!o.isNull());
QCOMPARE(o->objectName(), "blue/0.5");
// Don't crash when retrieving the (possibly non-existent) metaobject for the script.
for (int i = QMetaType::User + 1; QMetaType::isRegistered(i); ++i)
QMetaType(i).metaObject();
}
void tst_qqmlengine::registerModule()
{
// Make sure that registerModule() doesn't crash when invoked via QQmlEngine
QQmlEngine engine;
QVERIFY(engine.registerModule("magic", QJSValue(63)));
}
QTEST_MAIN(tst_qqmlengine)
#include "tst_qqmlengine.moc"