1910 lines
66 KiB
C++
1910 lines
66 KiB
C++
// 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();
|
|
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
|
|
{
|
|
QString path = url.path();
|
|
|
|
if ((url.scheme() != QStringLiteral("file") && url.scheme() != QStringLiteral("qrc"))
|
|
|| path.contains("QtQml")) {
|
|
return url;
|
|
}
|
|
|
|
if (!m_interceptionPoints.contains(d))
|
|
return url;
|
|
|
|
if (path.endsWith("Test.2/qmldir")) {
|
|
// Special case
|
|
QUrl url = m_base;
|
|
url.setPath(m_base.path() + "interception/module/intercepted/qmldir");
|
|
return url;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
path.insert(lastSlash, QStringLiteral("/intercepted"));
|
|
|
|
QUrl ret = url;
|
|
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"));
|
|
}
|
|
|
|
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"
|