diff options
-rw-r--r-- | .qmake.conf | 2 | ||||
-rw-r--r-- | examples/webchannel/qwclient/README | 18 | ||||
-rw-r--r-- | examples/webchannel/qwclient/package.json | 14 | ||||
-rwxr-xr-x | examples/webchannel/qwclient/qwclient.js | 134 | ||||
-rw-r--r-- | examples/webchannel/qwclient/qwclient.pro | 9 | ||||
-rw-r--r-- | examples/webchannel/webchannel.pro | 1 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.cpp | 175 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher_p.h | 40 | ||||
-rw-r--r-- | src/webchannel/qwebchannel.js | 29 | ||||
-rw-r--r-- | tests/auto/qml/Client.qml | 120 | ||||
-rw-r--r-- | tests/auto/qml/qml.pro | 7 | ||||
-rw-r--r-- | tests/auto/qml/tst_multiclient.qml | 144 | ||||
-rw-r--r-- | tests/auto/qml/tst_webchannel.qml | 63 | ||||
-rw-r--r-- | tests/auto/qml/tst_webchannelseparation.qml | 358 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.cpp | 10 |
15 files changed, 964 insertions, 160 deletions
diff --git a/.qmake.conf b/.qmake.conf index 656f781..b5923cf 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -1,4 +1,4 @@ load(qt_build_config) CONFIG += qt_example_installs warning_clean -MODULE_VERSION = 5.4.2 +MODULE_VERSION = 5.5.0 diff --git a/examples/webchannel/qwclient/README b/examples/webchannel/qwclient/README new file mode 100644 index 0000000..2b90427 --- /dev/null +++ b/examples/webchannel/qwclient/README @@ -0,0 +1,18 @@ +A REPL client for any qwebchannel service using a websocket transport. + +A nice tool for testing qwebchannel endpoints. + +Install: + - qmake && make from the qwebchannel sources or examples. + - npm install + +Usage: + - qwclient.js <autoconnect server> + - openChannel(url) in the REPL to open a new connection, multiple channels are OK + - channel object lists are aliased to c<channelnumber> i.e. c0, c1, ... + + Example using the standalone example server: + - Launch standalone example server + - qwclient localhost:12345 + - c0.receiveText('test') + - 'test' should be displayed in the standalone server UI diff --git a/examples/webchannel/qwclient/package.json b/examples/webchannel/qwclient/package.json new file mode 100644 index 0000000..f5b9db0 --- /dev/null +++ b/examples/webchannel/qwclient/package.json @@ -0,0 +1,14 @@ +{ + "name": "qwclient", + "description": "A generic REPL client for qwebchannel services using a websocket transport.", + "author": "Sumedha Widyadharma <sumedha.widyadharma@basyskom.com>", + "version": "0.0.1", + "dependencies": { + "faye-websocket": "0.7.x" + }, + "engine": "node 0.10.x", + "repository": { + "type": "git", + "url": "git://gitorious.org/qt/qtwebchannel.git" + } +} diff --git a/examples/webchannel/qwclient/qwclient.js b/examples/webchannel/qwclient/qwclient.js new file mode 100755 index 0000000..c2922b3 --- /dev/null +++ b/examples/webchannel/qwclient/qwclient.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/**************************************************************************** +** +** Copyright (C) 2014 basysKom GmbH, author Sumedha Widyadharma <sumedha.widyadharma@basyskom.com> +** Copyright (C) 2014 basysKom GmbH, author Lutz Schönemann <lutz.schoenemann@basyskom.com> +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +'use strict'; +var repl = require('repl'); +var WebSocket = require('faye-websocket').Client; +var QWebChannel = new require('./qwebchannel.js').QWebChannel; + +var serverAddress = 'ws://localhost:12345'; +var channels = []; + +var autoConnect = process.argv.pop(); +if (autoConnect === __filename) { + autoConnect = false; +} + +var openChannel = function (address) { + // this should be bound to the repl + var self = this; + address = address ? address : serverAddress; + if (address.indexOf('://') === -1) { + address = 'ws://' + address; + } + + var ws = new WebSocket(address); + + ws.on('open', function (event) { + var transport = { + onmessage: function (data) {}, + send: function (data) { + ws.send(data, {binary: false}); + } + }; + ws.on('message', function (event) { + transport.onmessage(event); + }); // onmessage + + var webChannel = new QWebChannel(transport, function (channel) { + channels.push(channel); + var channelIdx = (channels.length - 1); + console.log('channel opened', channelIdx); + // Create a nice alias to access this channels objects + self.context['c' + channelIdx] = channel.objects; + + ws.on('close', function () { + for (var i = 0; i < channels.length; ++i) { + if (channels[i] === channel) { + console.log('channel closed', i); + channels[i] = null; + return; + } + } + }); // onclose + }); // new QWebChannel + }); // onopen + + ws.on('error', function (error) { + console.log('websocket error', error.message); + }); +}; // openChannel + +var setupRepl = function() { + var r = repl.start({ + prompt: "webchannel> ", + input: process.stdin, + output: process.stdout + }); + + r.context.serverAddress = serverAddress; + r.context.openChannel = openChannel.bind(r); + r.context.channels = channels; + + r.context.lsObjects = function() { + channels.forEach(function(channel){ + console.log('Channel ' + channel); + Object.keys(channel.objects); + }); + } + return r; +} + +var welcome = function() { + console.log('Welcome to the qwebchannel/websocket REPL.'); + console.log('Use openChannel(url) to connect to a service.'); + console.log('For the standalone example, just openChannel() should suffice.'); + console.log('Opened channels have their objects aliased to c<channel number>, i.e. c0'); + console.log('So for the standalone example try: c0.dialog.receiveText(\'hello world\')'); +} + +welcome(); +var repl = setupRepl(); + +if (autoConnect) { + repl.context.openChannel(autoConnect); +} diff --git a/examples/webchannel/qwclient/qwclient.pro b/examples/webchannel/qwclient/qwclient.pro new file mode 100644 index 0000000..587571a --- /dev/null +++ b/examples/webchannel/qwclient/qwclient.pro @@ -0,0 +1,9 @@ +TEMPLATE = aux + +exampleassets.files += \ + qwclient.js \ + package.json \ + README + +exampleassets.path = $$[QT_INSTALL_EXAMPLES]/qwebchannel/qwclient +include(../exampleassets.pri) diff --git a/examples/webchannel/webchannel.pro b/examples/webchannel/webchannel.pro index 30df5a5..7d3db8d 100644 --- a/examples/webchannel/webchannel.pro +++ b/examples/webchannel/webchannel.pro @@ -8,4 +8,5 @@ qtHaveModule(websockets) { } SUBDIRS += nodejs \ + qwclient \ chatclient-html diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp index b92cf51..0c22798 100644 --- a/src/webchannel/qmetaobjectpublisher.cpp +++ b/src/webchannel/qmetaobjectpublisher.cpp @@ -41,6 +41,7 @@ #include <QDebug> #include <QJsonObject> #include <QJsonArray> +#include <QJSValue> #include <QUuid> QT_BEGIN_NAMESPACE @@ -73,6 +74,15 @@ const QString KEY_ARGS = QStringLiteral("args"); const QString KEY_PROPERTY = QStringLiteral("property"); const QString KEY_VALUE = QStringLiteral("value"); +QJsonObject createResponse(const QJsonValue &id, const QJsonValue &data) +{ + QJsonObject response; + response[KEY_TYPE] = TypeResponse; + response[KEY_ID] = id; + response[KEY_DATA] = data; + return response; +} + /// TODO: what is the proper value here? const int PROPERTY_UPDATE_INTERVAL = 50; } @@ -83,7 +93,6 @@ QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel) , signalHandler(this) , clientIsIdle(false) , blockUpdates(false) - , pendingInit(false) , propertyUpdatesInitialized(false) { } @@ -102,11 +111,11 @@ void QMetaObjectPublisher::registerObject(const QString &id, QObject *object) qWarning("Registered new object after initialization, existing clients won't be notified!"); // TODO: send a message to clients that an object was added } - initializePropertyUpdates(object, classInfoForObject(object)); + initializePropertyUpdates(object, classInfoForObject(object, Q_NULLPTR)); } } -QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object) +QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object, QWebChannelAbstractTransport *transport) { QJsonObject data; if (!object) { @@ -154,7 +163,7 @@ QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object) prop.name(), object->metaObject()->className()); } propertyInfo.append(signalInfo); - propertyInfo.append(wrapResult(prop.read(object))); + propertyInfo.append(wrapResult(prop.read(object), transport)); qtProperties.append(propertyInfo); } for (int i = 0; i < metaObject->methodCount(); ++i) { @@ -211,29 +220,21 @@ void QMetaObjectPublisher::setClientIsIdle(bool isIdle) } } -void QMetaObjectPublisher::initializeClients() +QJsonObject QMetaObjectPublisher::initializeClient(QWebChannelAbstractTransport *transport) { - if (!webChannel) { - return; - } - QJsonObject objectInfos; { const QHash<QString, QObject *>::const_iterator end = registeredObjects.constEnd(); for (QHash<QString, QObject *>::const_iterator it = registeredObjects.constBegin(); it != end; ++it) { - const QJsonObject &info = classInfoForObject(it.value()); + const QJsonObject &info = classInfoForObject(it.value(), transport); if (!propertyUpdatesInitialized) { initializePropertyUpdates(it.value(), info); } objectInfos[it.key()] = info; } } - QJsonObject message; - message[KEY_TYPE] = TypeInit; - message[KEY_DATA] = objectInfos; - broadcastMessage(message); propertyUpdatesInitialized = true; - pendingInit = false; + return objectInfos; } void QMetaObjectPublisher::initializePropertyUpdates(const QObject *const object, const QJsonObject &objectInfo) @@ -275,12 +276,14 @@ void QMetaObjectPublisher::sendPendingPropertyUpdates() } QJsonArray data; + QHash<QWebChannelAbstractTransport*, QJsonArray> specificUpdates; // convert pending property updates to JSON data const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd(); for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) { const QObject *object = it.key(); const QMetaObject *const metaObject = object->metaObject(); + const QString objectId = registeredObjectIds.value(object); const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object); // maps property name to current property value QJsonObject properties; @@ -292,26 +295,47 @@ void QMetaObjectPublisher::sendPendingPropertyUpdates() foreach (const int propertyIndex, objectsSignalToPropertyMap.value(sigIt.key())) { const QMetaProperty &property = metaObject->property(propertyIndex); Q_ASSERT(property.isValid()); - properties[QString::number(propertyIndex)] = wrapResult(property.read(object)); + properties[QString::number(propertyIndex)] = wrapResult(property.read(object), Q_NULLPTR, objectId); } sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value()); } QJsonObject obj; - obj[KEY_OBJECT] = registeredObjectIds.value(object); + obj[KEY_OBJECT] = objectId; obj[KEY_SIGNALS] = sigs; obj[KEY_PROPERTIES] = properties; - data.push_back(obj); + + // if the object is auto registered, just send the update only to clients which know this object + if (wrappedObjects.contains(objectId)) { + foreach (QWebChannelAbstractTransport *transport, wrappedObjects.value(objectId).transports) { + QJsonArray &arr = specificUpdates[transport]; + arr.push_back(obj); + } + } else { + data.push_back(obj); + } } pendingPropertyUpdates.clear(); QJsonObject message; message[KEY_TYPE] = TypePropertyUpdate; - message[KEY_DATA] = data; - setClientIsIdle(false); - broadcastMessage(message); + + // data does not contain specific updates + if (!data.isEmpty()) { + setClientIsIdle(false); + + message[KEY_DATA] = data; + broadcastMessage(message); + } + + // send every property update which is not supposed to be broadcasted + const QHash<QWebChannelAbstractTransport*, QJsonArray>::const_iterator suend = specificUpdates.constEnd(); + for (QHash<QWebChannelAbstractTransport*, QJsonArray>::const_iterator it = specificUpdates.constBegin(); it != suend; ++it) { + message[KEY_DATA] = it.value(); + it.key()->sendMessage(message); + } } -QJsonValue QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args) { const QMetaMethod &method = object->metaObject()->method(methodIndex); @@ -362,7 +386,7 @@ QJsonValue QMetaObjectPublisher::invokeMethod(QObject *const object, const int m arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]); - return wrapResult(returnValue); + return returnValue; } void QMetaObjectPublisher::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments) @@ -379,10 +403,18 @@ void QMetaObjectPublisher::signalEmitted(const QObject *object, const int signal message[KEY_OBJECT] = objectName; message[KEY_SIGNAL] = signalIndex; if (!arguments.isEmpty()) { - message[KEY_ARGS] = wrapList(arguments); + message[KEY_ARGS] = wrapList(arguments, Q_NULLPTR, objectName); } message[KEY_TYPE] = TypeSignal; - broadcastMessage(message); + + // if the object is wrapped, just send the response to clients which know this object + if (wrappedObjects.contains(objectName)) { + foreach (QWebChannelAbstractTransport *transport, wrappedObjects.value(objectName).transports) { + transport->sendMessage(message); + } + } else { + broadcastMessage(message); + } if (signalIndex == s_destroyedSignalIndex) { objectDestroyed(object); @@ -399,65 +431,87 @@ void QMetaObjectPublisher::objectDestroyed(const QObject *object) { const QString &id = registeredObjectIds.take(object); Q_ASSERT(!id.isEmpty()); - bool removed = registeredObjects.remove(id); + bool removed = registeredObjects.remove(id) + || wrappedObjects.remove(id); Q_ASSERT(removed); Q_UNUSED(removed); signalHandler.remove(object); signalToPropertyMap.remove(object); pendingPropertyUpdates.remove(object); - wrappedObjects.remove(object); } -QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result) +// NOTE: transport can be a nullptr +// in such a case, we need to ensure that the property is registered to +// the target transports of the parentObjectId +QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelAbstractTransport *transport, + const QString &parentObjectId) { if (QObject *object = result.value<QObject *>()) { - QJsonObject objectInfo; - objectInfo[KEY_QOBJECT] = true; QString id = registeredObjectIds.value(object); + QJsonObject classInfo; if (id.isEmpty()) { // neither registered, nor wrapped, do so now id = QUuid::createUuid().toString(); + Q_ASSERT(!registeredObjects.contains(id)); + + classInfo = classInfoForObject(object, transport); registeredObjectIds[object] = id; - registeredObjects[id] = object; - - QJsonObject info = classInfoForObject(object); - wrappedObjects[object] = info; - objectInfo[KEY_DATA] = info; - if (propertyUpdatesInitialized) { - // if other objects are initialized already, do the same here - initializePropertyUpdates(object, info); + + ObjectInfo oi(object, classInfo); + if (transport) { + oi.transports.append(transport); + } else { + // use the transports from the parent object + oi.transports = wrappedObjects.value(parentObjectId).transports; + // or fallback to all transports if the parent is not wrapped + if (oi.transports.isEmpty()) + oi.transports = webChannel->d_func()->transports; } - } else if (wrappedObjects.contains(object)) { - // if this object was wrapped, send the full class info - // this is required for proper multi-client support - objectInfo[KEY_DATA] = wrappedObjects.value(object); + wrappedObjects.insert(id, oi); + + initializePropertyUpdates(object, classInfo); + } else if (wrappedObjects.contains(id)) { + Q_ASSERT(object == wrappedObjects.value(id).object); + // check if this transport is already assigned to the object + if (transport && !wrappedObjects.value(id).transports.contains(transport)) + wrappedObjects[id].transports.append(transport); + classInfo = wrappedObjects.value(id).classinfo; } + QJsonObject objectInfo; + objectInfo[KEY_QOBJECT] = true; objectInfo[KEY_ID] = id; + if (!classInfo.isEmpty()) + objectInfo[KEY_DATA] = classInfo; return objectInfo; + } else if (result.canConvert<QJSValue>()) { + // Workaround for keeping QJSValues from QVariant. + // Calling QJSValue::toVariant() converts JS-objects/arrays to QVariantMap/List + // instead of stashing a QJSValue itself into a variant. + // TODO: Improve QJSValue-QJsonValue conversion in Qt. + return wrapResult(result.value<QJSValue>().toVariant(), transport, parentObjectId); } else if (result.canConvert<QVariantList>()) { // recurse and potentially wrap contents of the array - return wrapList(result.toList()); + return wrapList(result.toList(), transport); } - // no need to wrap this return QJsonValue::fromVariant(result); } -QJsonArray QMetaObjectPublisher::wrapList(const QVariantList &list) +QJsonArray QMetaObjectPublisher::wrapList(const QVariantList &list, QWebChannelAbstractTransport *transport, const QString &parentObjectId) { QJsonArray array; foreach (const QVariant &arg, list) { - array.append(wrapResult(arg)); + array.append(wrapResult(arg, transport, parentObjectId)); } return array; } void QMetaObjectPublisher::deleteWrappedObject(QObject *object) const { - if (!wrappedObjects.contains(object)) { + if (!wrappedObjects.contains(registeredObjectIds.value(object))) { qWarning() << "Not deleting non-wrapped object" << object; return; } @@ -492,17 +546,21 @@ void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannel if (type == TypeIdle) { setClientIsIdle(true); } else if (type == TypeInit) { - if (!blockUpdates) { - initializeClients(); - } else { - pendingInit = true; + if (!message.contains(KEY_ID)) { + qWarning("JSON message object is missing the id property: %s", + QJsonDocument(message).toJson().constData()); + return; } + transport->sendMessage(createResponse(message.value(KEY_ID), initializeClient(transport))); } else if (type == TypeDebug) { static QTextStream out(stdout); out << "DEBUG: " << message.value(KEY_DATA).toString() << endl; } else if (message.contains(KEY_OBJECT)) { const QString &objectName = message.value(KEY_OBJECT).toString(); QObject *object = registeredObjects.value(objectName); + if (!object) + object = wrappedObjects.value(objectName).object; + if (!object) { qWarning() << "Unknown object encountered" << objectName; return; @@ -514,11 +572,10 @@ void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannel QJsonDocument(message).toJson().constData()); return; } - QJsonObject response; - response[KEY_TYPE] = TypeResponse; - response[KEY_ID] = message.value(KEY_ID); - response[KEY_DATA] = invokeMethod(object, message.value(KEY_METHOD).toInt(-1), message.value(KEY_ARGS).toArray()); - transport->sendMessage(response); + + transport->sendMessage(createResponse(message.value(KEY_ID), + wrapResult(invokeMethod(object, message.value(KEY_METHOD).toInt(-1), + message.value(KEY_ARGS).toArray()), transport))); } else if (type == TypeConnectToSignal) { signalHandler.connectTo(object, message.value(KEY_SIGNAL).toInt(-1)); } else if (type == TypeDisconnectFromSignal) { @@ -544,11 +601,7 @@ void QMetaObjectPublisher::setBlockUpdates(bool block) blockUpdates = block; if (!blockUpdates) { - if (pendingInit) { - initializeClients(); - } else { - sendPendingPropertyUpdates(); - } + sendPendingPropertyUpdates(); } else if (timer.isActive()) { timer.stop(); } diff --git a/src/webchannel/qmetaobjectpublisher_p.h b/src/webchannel/qmetaobjectpublisher_p.h index 05f33bd..359135c 100644 --- a/src/webchannel/qmetaobjectpublisher_p.h +++ b/src/webchannel/qmetaobjectpublisher_p.h @@ -40,6 +40,8 @@ #include <QStringList> #include <QMetaObject> #include <QBasicTimer> +#include <QPointer> +#include <QJsonObject> #include "qwebchannelglobal.h" @@ -70,7 +72,6 @@ class QWebChannelAbstractTransport; class Q_WEBCHANNEL_EXPORT QMetaObjectPublisher : public QObject { Q_OBJECT - public: explicit QMetaObjectPublisher(QWebChannel *webChannel); virtual ~QMetaObjectPublisher(); @@ -94,7 +95,7 @@ public: /** * Serialize the QMetaObject of @p object and return it in JSON form. */ - QJsonObject classInfoForObject(const QObject *object); + QJsonObject classInfoForObject(const QObject *object, QWebChannelAbstractTransport *transport); /** * Set the client to idle or busy, based on the value of @p isIdle. @@ -108,7 +109,7 @@ public: * * Furthermore, if that was not done already, connect to their property notify signals. */ - void initializeClients(); + QJsonObject initializeClient(QWebChannelAbstractTransport *transport); /** * Go through all properties of the given object and connect to their notify signal. @@ -135,7 +136,7 @@ public: * The return value of the method invocation is then serialized and a response message * is returned. */ - QJsonValue invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args); + QVariant invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args); /** * Callback of the signalHandler which forwards the signal invocation to the webchannel clients. @@ -155,14 +156,16 @@ public: * * All other input types are returned as-is. */ - QJsonValue wrapResult(const QVariant &result); + QJsonValue wrapResult(const QVariant &result, QWebChannelAbstractTransport *transport, + const QString &parentObjectId = QString()); /** * Convert a list of variant values for consumption by the client. * * This properly handles QML values and also wraps the result if required. */ - QJsonArray wrapList(const QVariantList &list); + QJsonArray wrapList(const QVariantList &list, QWebChannelAbstractTransport *transport, + const QString &parentObjectId = QString()); /** * Invoke delete later on @p object. @@ -200,10 +203,6 @@ private: // true when no property updates should be sent, false otherwise bool blockUpdates; - // true when at least one client needs to be initialized, - // i.e. when a Qt.init came in which was not handled yet. - bool pendingInit; - // true when at least one client was initialized and thus // the property updates have been initialized and the // object info map set. @@ -215,6 +214,24 @@ private: // Map the registered objects to their id. QHash<const QObject *, QString> registeredObjectIds; + // Groups individually wrapped objects with their class information and the transports that have access to it. + struct ObjectInfo + { + ObjectInfo() + : object(Q_NULLPTR) + {} + ObjectInfo(QObject *o, const QJsonObject &i) + : object(o) + , classinfo(i) + {} + QObject *object; + QJsonObject classinfo; + QVector<QWebChannelAbstractTransport*> transports; + }; + + // Map of objects wrapped from invocation returns + QHash<QString, ObjectInfo> wrappedObjects; + // Map of objects to maps of signal indices to a set of all their property indices. // The last value is a set as a signal can be the notify signal of multiple properties. typedef QHash<int, QSet<int> > SignalToPropertyNameMap; @@ -226,9 +243,6 @@ private: typedef QHash<const QObject *, SignalToArgumentsMap> PendingPropertyUpdates; PendingPropertyUpdates pendingPropertyUpdates; - // Maps wrapped object to class info - QHash<const QObject *, QJsonObject> wrappedObjects; - // Aggregate property updates since we get multiple Qt.idle message when we have multiple // clients. They all share the same QWebProcess though so we must take special care to // prevent message flooding. diff --git a/src/webchannel/qwebchannel.js b/src/webchannel/qwebchannel.js index 472330e..4a1c784 100644 --- a/src/webchannel/qwebchannel.js +++ b/src/webchannel/qwebchannel.js @@ -82,9 +82,6 @@ var QWebChannel = function(transport, initCallback) case QWebChannelMessageTypes.propertyUpdate: channel.handlePropertyUpdate(data); break; - case QWebChannelMessageTypes.init: - channel.handleInit(data); - break; default: console.error("invalid message received:", message.data); break; @@ -149,17 +146,14 @@ var QWebChannel = function(transport, initCallback) channel.exec({type: QWebChannelMessageTypes.idle}); } - // prevent multiple initialization which might happen with multiple webchannel clients. - this.initialized = false; - this.handleInit = function(message) + this.debug = function(message) { - if (channel.initialized) { - return; - } - channel.initialized = true; - for (var objectName in message.data) { - var data = message.data[objectName]; - var object = new QObject(objectName, data, channel); + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (var objectName in data) { + var object = new QObject(objectName, data[objectName], channel); } // now unwrap properties, which might reference other registered objects for (var objectName in channel.objects) { @@ -169,14 +163,7 @@ var QWebChannel = function(transport, initCallback) initCallback(channel); } channel.exec({type: QWebChannelMessageTypes.idle}); - } - - this.debug = function(message) - { - channel.send({type: QWebChannelMessageTypes.debug, data: message}); - }; - - channel.exec({type: QWebChannelMessageTypes.init}); + }); }; function QObject(name, data, webChannel) diff --git a/tests/auto/qml/Client.qml b/tests/auto/qml/Client.qml index 20da8f6..ff2824b 100644 --- a/tests/auto/qml/Client.qml +++ b/tests/auto/qml/Client.qml @@ -39,12 +39,15 @@ import QtWebChannel.Tests 1.0 import "qrc:///qtwebchannel/qwebchannel.js" as JSClient Item { + id: root + TestTransport { id: serverTransport } readonly property var serverTransport: serverTransport property var clientMessages: [] + property var serverMessages: [] property bool debug: false @@ -55,24 +58,21 @@ Item { function send(message) { - if (debug) { - console.log("client posts message: ", message, "is idle:", webChannel.clientIsIdle()); - } - clientMessages.push(message); + if (debug) + console.log("client", (root.objectName ? "(" + root.objectName + ")" : ""), "posts message: ", message, "is idle:", webChannel.clientIsIdle()); + clientMessages.push(JSON.parse(message)); serverTransport.receiveMessage(message); - if (message && message.type && message.type === JSClient.QWebChannelMessageTypes.idle) { + if (message && message.type && message.type === JSClient.QWebChannelMessageTypes.idle) verify(webChannel.clientIsIdle()); - } } Component.onCompleted: { - serverTransport.sendMessageRequested.connect(function(message) { - if (debug) { - console.log("client received message: ", JSON.stringify(message)); - } - if (onmessage) { + serverTransport.sendMessageRequested.connect(function receive(message) { + if (debug) + console.log("client", (root.objectName ? "(" + root.objectName + ")" : ""), "received message:", JSON.stringify(message)); + serverMessages.push(message); + if (onmessage) onmessage({data:message}); - } }); } } @@ -86,51 +86,95 @@ Item { function cleanup() { clientMessages = []; + serverMessages = []; } - function awaitRawMessage() + function awaitRawMessage(from) { - for (var i = 0; i < 10 && !clientMessages.length; ++i) { + var messages; + if (!from || typeof from !== "string" || from == "client") { + from = "client"; + messages = clientMessages; + } else { + from = "server"; + messages = serverMessages; + } + + for (var i = 0; i < 10 && !messages.length; ++i) wait(10); + + var msg = messages.shift(); + if (debug) { + console.log((root.objectName ? "(" + root.objectName + ")" : ""), "shifting message " + from + "[" + messages.length + "]" + ":" + JSON.stringify(msg)); } - return clientMessages.shift(); + return msg; } - function awaitMessage() + function awaitMessage(from) { - var msg = awaitRawMessage() - if (debug) { - console.log("handling message: ", msg); - } - if (!msg) { + var msg = awaitRawMessage(from) + if (debug) + console.log((root.objectName ? "(" + root.objectName + ")" : ""), "handling message: ", JSON.stringify(msg)); + if (!msg) return false; + return msg; + } + + function await(type, from, skip) { + var msg; + do { + msg = awaitMessage(from); + if (!msg) { + console.trace(); + verify(msg); + } + } while (skip && (msg.type === JSClient.QWebChannelMessageTypes.idle)); + if (type !== null) { + if (!msg || msg.type != type) + console.trace(); + verify(msg); + compare(msg.type, type); } - return JSON.parse(msg); + return msg; + } + + function awaitInit() { + return await(JSClient.QWebChannelMessageTypes.init); + } + + function awaitIdle() { + return await(JSClient.QWebChannelMessageTypes.idle); + } + + function awaitMessageSkipIdle() { + return awaitFunc(null, null, true); } - function awaitInit() + function awaitServerInit() { + return await(JSClient.QWebChannelMessageTypes.init, "server"); + } + + function awaitSignal() { - var msg = awaitMessage(); - verify(msg); - verify(msg.type); - compare(msg.type, JSClient.QWebChannelMessageTypes.init); + return await(JSClient.QWebChannelMessageTypes.signal, "server"); } - function awaitIdle() + function awaitPropertyUpdate() { - var msg = awaitMessage(); - verify(msg); - compare(msg.type, JSClient.QWebChannelMessageTypes.idle); + return await(JSClient.QWebChannelMessageTypes.propertyUpdate, "server"); } - function awaitMessageSkipIdle() + function awaitResponse() { - var msg; - do { - msg = awaitMessage(); - verify(msg); - } while (msg.type === JSClient.QWebChannelMessageTypes.idle); - return msg; + return await(JSClient.QWebChannelMessageTypes.response, "server"); } + function skipToMessage(type, from, max) { + do { + var msg = awaitMessage(from); + if (msg && msg.type === type) + return msg + } while (--max > 0); + return false; + } } diff --git a/tests/auto/qml/qml.pro b/tests/auto/qml/qml.pro index b0c52b5..5dbac40 100644 --- a/tests/auto/qml/qml.pro +++ b/tests/auto/qml/qml.pro @@ -17,9 +17,14 @@ HEADERS += \ testwebchannel.h OTHER_FILES += \ + Client.qml \ WebChannelTest.qml \ tst_webchannel.qml \ tst_metaobjectpublisher.qml \ - tst_bench.qml + tst_bench.qml \ + tst_multiclient.qml TESTDATA = data/* + +DISTFILES += \ + tst_webchannelseparation.qml diff --git a/tests/auto/qml/tst_multiclient.qml b/tests/auto/qml/tst_multiclient.qml index b696391..4977e50 100644 --- a/tests/auto/qml/tst_multiclient.qml +++ b/tests/auto/qml/tst_multiclient.qml @@ -1,6 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +** Copyright (C) 2014 basysKom GmbH, info@basyskom.com, author Lutz Schönemann <lutz.schoenemann@basyskom.com> ** Contact: http://www.qt-project.org/legal ** ** This file is part of the QtWebChannel module of the Qt Toolkit. @@ -37,6 +38,8 @@ import QtTest 1.0 import QtWebChannel 1.0 import QtWebChannel.Tests 1.0 +import "qrc:///qtwebchannel/qwebchannel.js" as JSClient + TestCase { name: "MultiClient" @@ -62,18 +65,86 @@ TestCase { WebChannel.id: "foo" } + property var lastMethodArg + QtObject { + id: myObj + property int myProperty: 1 + + signal mySignal(var arg) + + function myMethod(arg) + { + lastMethodArg = arg; + } + + WebChannel.id: "myObj" + } + + QtObject { + id: myOtherObj + property var foo: 1 + property var bar: 1 + WebChannel.id: "myOtherObj" + } + + property var lastFactoryObj + property var createdFactoryObjects: [] + QtObject { + id: myFactory + + function cleanup() { + while (createdFactoryObjects.length) { + var obj = createdFactoryObjects.shift(); + if (obj) { + obj.destroy(); + } + } + } + + function create(id) + { + lastFactoryObj = component.createObject(myFactory, {objectName: id}); + createdFactoryObjects.push(lastFactoryObj); + return lastFactoryObj; + } + WebChannel.id: "myFactory" + } + + Component { + id: component + QtObject { + property var myProperty : 0 + function myMethod(arg) { + lastMethodArg = arg; + } + signal mySignal(var arg1, var arg2) + } + } + TestWebChannel { id: webChannel transports: [client1.serverTransport, client2.serverTransport] - registeredObjects: [foo] + registeredObjects: [foo, myObj, myOtherObj, myFactory] } function init() { + myObj.myProperty = 1 client1.cleanup(); client2.cleanup(); } + function cleanup() { + client1.debug = false; + client2.debug = false; + // delete all created objects + myFactory.cleanup(); + lastFactoryObj = undefined; + createdFactoryObjects = []; + // reschedule current task to end of event loop + wait(1); + } + function clientInitCallback(channel) { channel.objects.foo.ping.connect(function() { @@ -103,4 +174,75 @@ TestCase { compare(c1.pongAnswer, 1); compare(c2.pongAnswer, 2); } + + function test_autowrappedObjectsNotInInit() { + var testObj1; + var testObj1Id; + + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myFactory.create("testObj1", function (obj1) { + testObj1 = lastFactoryObj; + testObj1Id = obj1.__id__; + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + var channel2; + function createSecondChannel() { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + }); + } + + client1.awaitInit(); + var msg1 = client1.awaitMessage(); + compare(msg1.type, JSClient.QWebChannelMessageTypes.invokeMethod); // create + + client1.awaitIdle(); + + client2.awaitInit(); + client2.awaitIdle(); + + compare(typeof channel2.objects[testObj1Id], "undefined") + } + + function test_autowrappedObjectsNotBroadcasted() { + var testObj2; + var testObj2Id; + + var channel1 = client1.createChannel(function (channel1) { + // create second channel after first channel to make sure + // that a dynamically created object do not get exposed to + // existing channels + createSecondChannel(); + }); + var channel2; + function createSecondChannel() { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + channel2.objects.myFactory.create("testObj2", function (obj2) { + testObj2 = lastFactoryObj; + testObj2Id = obj2.__id__; + }); + }); + } + + client1.awaitInit(); + client1.awaitIdle(); + + client2.awaitInit(); + var msg2 = client2.awaitMessage(); + compare(msg2.type, JSClient.QWebChannelMessageTypes.invokeMethod); // create + + client2.awaitIdle(); + + compare(typeof channel1.objects[testObj2Id], "undefined") + } } diff --git a/tests/auto/qml/tst_webchannel.qml b/tests/auto/qml/tst_webchannel.qml index e7fcedc..3bfbfc9 100644 --- a/tests/auto/qml/tst_webchannel.qml +++ b/tests/auto/qml/tst_webchannel.qml @@ -66,15 +66,15 @@ TestCase { property var bar: 1 WebChannel.id: "myOtherObj" } + property var lastFactoryObj QtObject{ id: bar; objectName: "bar" } QtObject{ id: baz; objectName: "baz" } QtObject { id: myFactory - property var lastObj function create(id) { - lastObj = component.createObject(myFactory, {objectName: id}); - return lastObj; + lastFactoryObj = component.createObject(myFactory, {objectName: id}); + return lastFactoryObj; } property var objectInProperty: QtObject { objectName: "foo" @@ -145,7 +145,6 @@ TestCase { compare(myObj.myProperty, 3); client.awaitIdle(); // init - client.awaitIdle(); // property update // change property, should be propagated to HTML client and a message be send there myObj.myProperty = 2; @@ -242,12 +241,15 @@ TestCase { }); }); client.awaitInit(); + client.awaitResponse(); + // create testObj var msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); compare(msg.object, "myFactory"); - verify(myFactory.lastObj); - compare(myFactory.lastObj.objectName, "testObj"); + client.awaitResponse(); + verify(lastFactoryObj); + compare(lastFactoryObj.objectName, "testObj"); compare(channel.objects[testObjId].objectName, "testObj"); // mySignal connection @@ -255,20 +257,30 @@ TestCase { compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal); compare(msg.object, testObjId); + // set myProperty msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.setProperty); compare(msg.object, testObjId); - compare(myFactory.lastObj.myProperty, 42); + compare(lastFactoryObj.myProperty, 42); + // call myMethod msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); compare(msg.object, testObjId); compare(msg.args, ["foobar"]); + client.awaitResponse(); compare(lastMethodArg, "foobar"); client.awaitIdle(); - myFactory.lastObj.mySignal("foobar", 42); + // the server should eventually notify the client about the property update + client.awaitPropertyUpdate(); + + client.awaitIdle(); + + // trigger a signal and ensure it gets transmitted + lastFactoryObj.mySignal("foobar", 42); + client.awaitSignal(); // property should be wrapped compare(channel.objects.myFactory.objectInProperty.objectName, "foo"); @@ -283,10 +295,13 @@ TestCase { msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); compare(msg.object, testObjId); + client.awaitResponse(); + // now the signalArgs should also be set compare(signalArgs, {"0": "foobar", "1": 42}); - client.awaitIdle(); // destroyed signal + // and also a destroyed signal + client.awaitSignal(); compare(JSON.stringify(testObjBeforeDeletion), JSON.stringify({})); compare(JSON.stringify(testObjAfterDeletion), JSON.stringify({})); @@ -304,51 +319,61 @@ TestCase { }); }); client.awaitInit(); + client.awaitResponse(); + + // call to myFactory.create() + var msg = client.awaitMessage(); + compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); + client.awaitResponse(); - // ignore first message (call to myFactory.create()) - client.awaitMessage(); client.awaitIdle(); verify(testObj); var testObjId = testObj.__id__; testObj.deleteLater(); - var msg = client.awaitMessage(); + msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); compare(msg.object, testObjId); + client.awaitResponse(); + // destroyed signal + client.awaitSignal(); - // after receiving the destroyed signal the client deletes - // local objects and sends back a idle message - client.awaitIdle(); - - compare(myFactory.lastObj, null); + compare(lastFactoryObj, null); compare(typeof channel.objects[testObjId], "undefined"); } - function test_wrapper_propertyUpdateOfWrappedObjects() { + function test_wrapper_propertyUpdateOfWrappedObjects() + { var testObj; var testObjId; var channel = client.createChannel(function(channel) { channel.objects.myFactory.create("testObj", function(obj) { - testObj = myFactory.lastObj; + testObj = lastFactoryObj; testObjId = obj.__id__; }); }); client.awaitInit(); + client.awaitResponse(); // call to myFactory.create() var msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); + client.awaitResponse(); client.awaitIdle(); testObj.myProperty = 42; + client.awaitPropertyUpdate(); client.awaitIdle(); compare(channel.objects[testObjId].myProperty, 42); channel.objects[testObjId].deleteLater(); msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); + client.awaitResponse(); + // destroyed signal + client.awaitSignal(); } function test_disconnect() diff --git a/tests/auto/qml/tst_webchannelseparation.qml b/tests/auto/qml/tst_webchannelseparation.qml new file mode 100644 index 0000000..8a74243 --- /dev/null +++ b/tests/auto/qml/tst_webchannelseparation.qml @@ -0,0 +1,358 @@ +/**************************************************************************** +** +** Copyright (C) 2014 basysKom GmbH, info@basyskom.com, author Lutz Schönemann <lutz.schoenemann@basyskom.com> +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.0 +import QtTest 1.0 + +import QtWebChannel 1.0 +import QtWebChannel.Tests 1.0 +import "qrc:///qtwebchannel/qwebchannel.js" as JSClient + +TestCase { + name: "WebChannelSeparation" + + Client { + id: client1 + objectName: "client1" + } + Client { + id: client2 + objectName: "client2" + } + + property var lastMethodArg + + QtObject { + id: myObj + property int myProperty: 1 + + signal mySignal(var arg) + + function myMethod(arg) + { + lastMethodArg = arg; + } + + WebChannel.id: "myObj" + } + QtObject { + id: myOtherObj + property var foo: 1 + property var bar: 1 + WebChannel.id: "myOtherObj" + } + QtObject { + id: myObj2 + function myMethod() + { + // return a javascript object which is handled as an Object, but not a QObject + return { "obj1": { "name": "n1", "value": 1 }, "obj2" : {"name": "n2", "value": 2} }; + } + + WebChannel.id: "myObj2" + } + + QtObject { + id: myObj3 + + // always returns the same object + function getObject() + { + return myObj; + } + WebChannel.id: "myObj3" + } + + property var lastFactoryObj + property var createdFactoryObjects: [] + QtObject { + id: myFactory + + function cleanup() + { + while (createdFactoryObjects.length) { + var obj = createdFactoryObjects.shift(); + if (obj) { + obj.destroy(); + } + } + } + + function create(id) + { + lastFactoryObj = component.createObject(myFactory, {objectName: id}); + createdFactoryObjects.push(lastFactoryObj); + return lastFactoryObj; + } + WebChannel.id: "myFactory" + } + + Component { + id: component + QtObject { + property var myProperty : 0 + function myMethod(arg) + { + lastMethodArg = arg; + } + signal mySignal(var arg1, var arg2) + } + } + + TestWebChannel { + id: webChannel + transports: [client1.serverTransport, client2.serverTransport] + registeredObjects: [myObj, myOtherObj, myObj2, myObj3, myFactory] + } + + function init() + { + myObj.myProperty = 1 + client1.cleanup(); + client2.cleanup(); + } + + function cleanup() + { + client1.debug = false; + client2.debug = false; + // delete all created objects + myFactory.cleanup(); + lastFactoryObj = undefined; + createdFactoryObjects = []; + // reschedule current task to end of event loop + wait(1); + } + + function test_signalSeparation() + { + var testObj1; + var testObj1Id; + var testObj2; + var testObj2Id; + + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myFactory.create("testObj1", function (obj1) { + testObj1 = lastFactoryObj; + testObj1Id = obj1.__id__; + + obj1.mySignal.connect(function (arg1_1, arg1_2) { + console.debug("client 1 received signal 'mySignal' " + arg1_1); + }); + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + + var channel2; + function createSecondChannel() + { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + channel2.objects.myFactory.create("testObj2", function (obj2) { + testObj2 = lastFactoryObj; + testObj2Id = obj2.__id__; + obj2.mySignal.connect(function (arg2_1, arg2_2) { + console.debug("client 2 received signal 'mySignal'"); + }); + }); + }); + } + + client1.awaitInit(); + client1.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + client2.awaitInit(); + client2.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + // dismiss server messages + client1.serverMessages = []; + client2.serverMessages = []; + + // now everything is set-up + // and we can kick off a signal + testObj1.mySignal("foo", "bar"); + + var msg1 = client1.awaitSignal(); + compare(msg1.signal, 6); + + // look if there is a signal send to client2, which should not happen + var msg2 = client2.skipToMessage(1, "server", 10); + console.log("client2 received a signal. let's check that it does not come from testObj1"); + if (msg2 !== false) { + verify(msg2.object !== testObj1Id); + } + } + + function test_separationForSameObject() + { + var testObj1; + var testObj2; + var receivedSignal1 = false; + var receivedSignal2 = false; + + var channel2; + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myObj3.getObject(function (obj) { + testObj1 = obj; + + testObj1.mySignal.connect(function() { + receivedSignal1 = true; + }); + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + + function createSecondChannel() + { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + verify(channel2.objects.myObj2) + channel2.objects.myObj3.getObject(function (obj) { + testObj2 = obj; + testObj2.mySignal.connect(function() { + receivedSignal2 = true; + }); + + }); + }); + } + + client1.awaitInit(); + client1.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + client2.awaitInit(); + client2.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + // trigger signal, signal should be received by both channels + myObj.mySignal("foo", "bar"); + + verify(receivedSignal1, "Channel 1 missed signal") + verify(receivedSignal2, "Channel 2 missed signal") + } + + function test_propertyUpdateSeparation() + { + var testObj1; + var testObj1Id; + var testObj2; + var testObj2Id; + + var channel1 = client1.createChannel(function (channel1) { + channel1.objects.myFactory.create("testObj1", function (obj1) { + testObj1 = lastFactoryObj; + testObj1Id = obj1.__id__; + + obj1.myPropertyChanged.connect(function (arg1_1) { + console.debug("client 1 received property update 'myProperty' " + obj1.myProperty); + }); + + // create second channel after factory has created first + // object to make sure that a dynamically created object + // exists but does not get exposed to new channels + createSecondChannel(); + }); + }); + + var channel2; + function createSecondChannel() + { + // dismiss all messges received before channel creation + client2.cleanup(); + + channel2 = client2.createChannel(function (channel2) { + channel2.objects.myFactory.create("testObj2", function (obj2) { + testObj2 = lastFactoryObj; + testObj2Id = obj2.__id__; + obj2.myPropertyChanged.connect(function (arg1_1) { + console.debug("client 2 received property update 'myProperty' " + obj2.myProperty); + }); + }); + }); + } + + client1.awaitInit(); + client1.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + client2.awaitInit(); + client2.skipToMessage(JSClient.QWebChannelMessageTypes.idle); + + // dismiss server messages + client1.serverMessages = []; + client2.serverMessages = []; + + // now everything is set-up + // and we can kick off a property change + testObj1.myProperty = 5; + + var msg1 = client1.awaitPropertyUpdate(); + compare(msg1.type, JSClient.QWebChannelMessageTypes.propertyUpdate); + + //look if there is a propertyUpdate sent to client2, which should not happen + var msg2 = client2.skipToMessage(2, "server", 10); + console.log("client2 received a propertyUpdate. let's check that it does not come from testObj1"); + if (msg2 !== false) { + verify(msg2.object !== testObj1Id); + } + } + + function test_returnNonQObject() + { + var retObj; + + var channel = client1.createChannel(function (channel) { + channel.objects.myObj2.myMethod(function(result) { + retObj = result; + }); + }); + + client1.awaitInit(); + + var msg1 = client1.awaitMessage(); + compare(msg1.type, JSClient.QWebChannelMessageTypes.invokeMethod); // create + + msg1 = client1.awaitIdle(); + verify(retObj["obj1"]["name"]); + } +} + diff --git a/tests/auto/webchannel/tst_webchannel.cpp b/tests/auto/webchannel/tst_webchannel.cpp index 3304293..3d77b42 100644 --- a/tests/auto/webchannel/tst_webchannel.cpp +++ b/tests/auto/webchannel/tst_webchannel.cpp @@ -93,7 +93,7 @@ void TestWebChannel::testDeregisterObjects() channel.registerObject(testObject.objectName(), &testObject); channel.connectTo(m_dummyTransport); - channel.d_func()->publisher->initializeClients(); + channel.d_func()->publisher->initializeClient(m_dummyTransport); QJsonObject connectMessage = QJsonDocument::fromJson(("{\"type\": 7," @@ -113,7 +113,7 @@ void TestWebChannel::testInfoForObject() obj.setObjectName("myTestObject"); QWebChannel channel; - const QJsonObject info = channel.d_func()->publisher->classInfoForObject(&obj); + const QJsonObject info = channel.d_func()->publisher->classInfoForObject(&obj, m_dummyTransport); QCOMPARE(info.keys(), QStringList() << "enums" << "methods" << "properties" << "signals"); @@ -291,7 +291,7 @@ void TestWebChannel::benchClassInfo() QBENCHMARK { foreach (const QObject *object, objects) { - channel.d_func()->publisher->classInfoForObject(object); + channel.d_func()->publisher->classInfoForObject(object, m_dummyTransport); } } } @@ -306,7 +306,7 @@ void TestWebChannel::benchInitializeClients() QMetaObjectPublisher *publisher = channel.d_func()->publisher; QBENCHMARK { - publisher->initializeClients(); + publisher->initializeClient(m_dummyTransport); publisher->propertyUpdatesInitialized = false; publisher->signalToPropertyMap.clear(); @@ -328,7 +328,7 @@ void TestWebChannel::benchPropertyUpdates() } channel.registerObjects(objects); - channel.d_func()->publisher->initializeClients(); + channel.d_func()->publisher->initializeClient(m_dummyTransport); QBENCHMARK { foreach (BenchObject *obj, objectList) { |