Revision control

++ // Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ import QtQuick
++ import QtTest
++
++ import QtWebChannel
++ import QtWebChannel.Tests
++ import "qrc:///qtwebchannel/qwebchannel.js" as JSClient
++
++ TestCase {
++ name: "WebChannel"
++
++ Client {
++ id: client
++ }
++
++ property var lastMethodArg
++
++ QtObject {
++ id: myObj
++ property int myProperty: 1
++
++ signal mySignal(var arg, QtObject object)
++
++ function myMethod(arg)
++ {
++ lastMethodArg = arg;
++ }
++
++ WebChannel.id: "myObj"
++ }
++ QtObject {
++ id: myOtherObj
++ property var foo: 1
++ property var bar: 1
++ WebChannel.id: "myOtherObj"
++ }
++ QtObject {
++ id: myValueObj
++ property var value: undefined
++ WebChannel.id: "myValueObj"
++ }
++ property var lastFactoryObj
++ QtObject{ id: bar; objectName: "bar" }
++ QtObject{ id: baz; objectName: "baz" }
++ QtObject {
++ id: myFactory
++ function create(id)
++ {
++ lastFactoryObj = component.createObject(myFactory, {objectName: id});
++ return lastFactoryObj;
++ }
++ function switchObject() {
++ otherObject = myOtherObj;
++ }
++ property var objectInProperty: QtObject {
++ objectName: "foo"
++ }
++ property var otherObject: myObj
++ property var objects: [ bar, baz ];
++ WebChannel.id: "myFactory"
++ }
++ Component {
++ id: component
++ QtObject {
++ property var myProperty : 0
++ function myMethod(arg) {
++ lastMethodArg = arg;
++ return myProperty;
++ }
++ signal mySignal(var arg1, var arg2)
++ }
++ }
++
++ TestObject {
++ id: testObject
++ WebChannel.id: "testObject"
++ }
++
++ TestWebChannel {
++ id: webChannel
++ transports: [client.serverTransport]
++ registeredObjects: [myObj, myOtherObj, myValueObj, myFactory, testObject]
++ }
++ WebChannel {
++ id: nullTest
++ }
++
++ function initChannel() {
++ client.serverTransport.receiveMessage(JSON.stringify({type: JSClient.QWebChannelMessageTypes.idle}));
++ webChannel.blockUpdates = true;
++ webChannel.blockUpdates = false;
++ }
++
++ function init()
++ {
++ myObj.myProperty = 1
++ // immediately send pending updates
++ // to avoid property changed signals
++ // during run of test case
++ initChannel();
++ client.cleanup();
++ }
++ function cleanup() {
++ // make tests a bit more strict and predictable
++ // by assuming that a test consumes all messages
++ initChannel();
++ compare(client.clientMessages.length, 0);
++ }
++
++ function test_notifyProperty()
++ {
++ compare(myObj.myProperty, 1);
++
++ var initialValue;
++ var changedValue;
++
++ var channel = client.createChannel(function(channel) {
++ initialValue = channel.objects.myObj.myProperty;
++ channel.objects.myObj.myPropertyChanged.connect(function() {
++ changedValue = channel.objects.myObj.myProperty;
++ });
++ // now trigger a write from the client side
++ channel.objects.myObj.myProperty = 3;
++ });
++
++ client.awaitInit();
++ var msg = client.awaitMessage();
++
++ compare(initialValue, 1);
++ compare(myObj.myProperty, 3);
++
++ client.awaitIdle(); // init
++
++ // change property, should be propagated to HTML client and a message be send there
++ myObj.myProperty = 2;
++ compare(myObj.myProperty, 2);
++ client.awaitIdle(); // property update
++ compare(changedValue, 2);
++ compare(channel.objects.myObj.myProperty, 2)
++ }
++
++ function test_bindableProperty()
++ {
++ compare(testObject.stringProperty, "foo");
++
++ var initialValue;
++
++ var channel = client.createChannel(function(channel) {
++ initialValue = channel.objects.testObject.stringProperty;
++ // now trigger a write from the client side
++ channel.objects.testObject.stringProperty = "bar";
++ });
++
++ client.awaitInit();
++ var msg = client.awaitMessage();
++
++ compare(initialValue, "foo");
++ compare(testObject.stringProperty, "bar");
++
++ client.awaitIdle(); // init
++
++ // Change property, should be propagated to HTML client.
++ // This is a bindable property only. Not change signal will be emitted
++ // because there is none.
++ testObject.stringProperty = "baz";
++ compare(testObject.stringProperty, "baz");
++ client.awaitIdle(); // property update
++ compare(channel.objects.testObject.stringProperty, "baz");
++ }
++
++ function test_method()
++ {
++ var channel = client.createChannel(function (channel) {
++ channel.objects.myObj.myMethod("test");
++ });
++
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod);
++ compare(msg.object, "myObj");
++ compare(msg.args, ["test"]);
++
++ compare(lastMethodArg, "test")
++
++ client.awaitIdle();
++ }
++
++ function test_signal()
++ {
++ var signalReceivedArg;
++ var signalReceivedObject;
++ var channel = client.createChannel(function(channel) {
++ channel.objects.myObj.mySignal.connect(function(arg, object) {
++ signalReceivedArg = arg;
++ signalReceivedObject = object;
++ });
++ });
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "myObj");
++
++ client.awaitIdle(); // initialization
++
++ myObj.mySignal("test", myObj);
++
++ compare(signalReceivedArg, "test");
++ compare(signalReceivedObject.__id__, "myObj");
++
++ var newObj = myFactory.create("newObj");
++ myObj.mySignal(newObj, newObj);
++
++ compare(signalReceivedArg.objectName, newObj.objectName);
++ compare(signalReceivedObject.objectName, newObj.objectName);
++ }
++
++ function test_grouping()
++ {
++ var receivedPropertyUpdates = 0;
++ var properties = 0;
++ var channel = client.createChannel(function(channel) {
++ var originalHandler = channel.handlePropertyUpdate;
++ channel.handlePropertyUpdate = function(message) {
++ originalHandler(message);
++ receivedPropertyUpdates++;
++ properties = [channel.objects.myObj.myProperty, channel.objects.myOtherObj.foo, channel.objects.myOtherObj.bar];
++ };
++ });
++ client.awaitInit();
++ client.awaitIdle();
++
++ // change properties a lot, we expect this to be grouped into a single update notification
++ for (var i = 0; i < 10; ++i) {
++ myObj.myProperty = i;
++ myOtherObj.foo = i;
++ myOtherObj.bar = i;
++ }
++
++ client.awaitIdle();
++ compare(receivedPropertyUpdates, 1);
++ compare(properties, [myObj.myProperty, myOtherObj.foo, myOtherObj.bar]);
++ verify(!client.awaitMessage());
++ }
++
++ function test_wrapper()
++ {
++ var signalArgs;
++ var testObjBeforeDeletion;
++ var testObjAfterDeletion;
++ var testObjId;
++ var testReturn;
++ var channel = client.createChannel(function(channel) {
++ channel.objects.myFactory.create("testObj", function(obj) {
++ testObjId = obj.__id__;
++ compare(channel.objects[testObjId], obj);
++ obj.mySignal.connect(function() {
++ signalArgs = arguments;
++ testObjBeforeDeletion = obj;
++ obj.deleteLater();
++ testObjAfterDeletion = obj;
++ });
++ obj.myProperty = 42;
++ obj.myMethod("foobar").then(function(result) {
++ testReturn = result;
++ });
++ });
++ });
++ client.awaitInit();
++ client.awaitResponse();
++
++ // create testObj
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod);
++ compare(msg.object, "myFactory");
++ client.awaitResponse();
++ verify(lastFactoryObj);
++ compare(lastFactoryObj.objectName, "testObj");
++ compare(channel.objects[testObjId].objectName, "testObj");
++
++ // mySignal connection
++ msg = client.awaitMessage();
++ 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(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();
++
++ // the server should eventually notify the client about the property update
++ client.awaitPropertyUpdate();
++
++ // check that the Promise from myMethod was resolved
++ // must happen after waiting for something so the Promise callback
++ // can execute
++ compare(testReturn, 42);
++
++ client.awaitIdle();
++
++ // property should be wrapped
++ compare(channel.objects.myFactory.objectInProperty.objectName, "foo");
++ // list property as well
++ compare(channel.objects.myFactory.objects.length, 2);
++ compare(channel.objects.myFactory.objects[0].objectName, "bar");
++ compare(channel.objects.myFactory.objects[1].objectName, "baz");
++ // map property as well
++ compare(channel.objects.testObject.objectMap.subObject.objectName,
++ "embedded");
++ // also works with properties that reference other registered objects
++ compare(channel.objects.myFactory.otherObject, channel.objects.myObj);
++
++ // change object property
++ channel.objects.myFactory.switchObject();
++ client.awaitMessage();
++ client.awaitResponse();
++ client.awaitIdle();
++ client.awaitPropertyUpdate();
++ compare(channel.objects.myFactory.otherObject, channel.objects.myOtherObj);
++
++ // trigger a signal and ensure it gets transmitted
++ lastFactoryObj.mySignal("foobar", 42);
++ client.awaitSignal();
++
++ // deleteLater call
++ 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});
++
++ // and also a destroyed signal
++ client.awaitSignal();
++
++ compare(JSON.stringify(testObjBeforeDeletion), JSON.stringify({}));
++ compare(JSON.stringify(testObjAfterDeletion), JSON.stringify({}));
++ compare(typeof channel.objects[testObjId], "undefined");
++ }
++
++ // test if returned QObjects get inserted into list of
++ // objects even if no callback function is set
++ function test_wrapper_wrapEveryQObject()
++ {
++ var testObj;
++ var channel = client.createChannel(function(channel) {
++ channel.objects.myFactory.create("testObj", function(obj) {
++ testObj = obj;
++ });
++ });
++ client.awaitInit();
++ client.awaitResponse();
++
++ // call to myFactory.create()
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod);
++ client.awaitResponse();
++
++ client.awaitIdle();
++
++ verify(testObj);
++ var testObjId = testObj.__id__;
++
++ testObj.deleteLater();
++ msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod);
++ compare(msg.object, testObjId);
++ client.awaitResponse();
++ // destroyed signal
++ client.awaitSignal();
++
++ compare(lastFactoryObj, null);
++ compare(typeof channel.objects[testObjId], "undefined");
++ }
++
++ function test_wrapper_propertyUpdateOfWrappedObjects()
++ {
++ var testObj;
++ var testObjId;
++ var channel = client.createChannel(function(channel) {
++ channel.objects.myFactory.create("testObj", function(obj) {
++ 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()
++ {
++ var signalArg;
++ var channel = client.createChannel(function(channel) {
++ channel.objects.myObj.mySignal.connect(function(arg) {
++ signalArg = arg;
++ channel.objects.myObj.mySignal.disconnect(this);
++ });
++ });
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "myObj");
++
++ client.awaitIdle();
++
++ myObj.mySignal(42, myObj);
++ compare(signalArg, 42);
++
++ msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.disconnectFromSignal);
++ compare(msg.object, "myObj");
++
++ myObj.mySignal(0, myObj);
++ compare(signalArg, 42);
++ }
++
++ function test_signalArgumentTypeConversion()
++ {
++ var signalArgs = [];
++ function logSignalArgs(arg) {
++ signalArgs.push(arg);
++ }
++ var channel = client.createChannel(function(channel) {
++ var testObject = channel.objects.testObject;
++ testObject.testSignalBool.connect(logSignalArgs);
++ testObject.testSignalInt.connect(logSignalArgs);
++ testObject.triggerSignals();
++ });
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "testObject");
++
++ msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "testObject");
++
++ msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod);
++ client.awaitIdle();
++
++ compare(signalArgs, [
++ true,
++ false,
++ 42,
++ 1,
++ 0
++ ]);
++ }
++
++ function test_multiConnect()
++ {
++ var signalArgs = [];
++ function logSignalArgs(arg) {
++ signalArgs.push(arg);
++ }
++ var channel = client.createChannel(function(channel) {
++ var testObject = channel.objects.testObject;
++ testObject.testSignalInt.connect(logSignalArgs);
++ testObject.testSignalInt.connect(logSignalArgs);
++ testObject.triggerSignals();
++ });
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "testObject");
++
++ msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod);
++ client.awaitIdle();
++
++ compare(signalArgs, [42, 42, 1, 1, 0, 0]);
++ }
++
++ function test_connectDuringEmit()
++ {
++ var cb1 = 0;
++ var cb2 = 0;
++ var channel = client.createChannel(function(channel) {
++ var myObj = channel.objects.myObj;
++ myObj.mySignal.connect(function() {
++ cb1++;
++ myObj.mySignal.connect(function() {
++ cb2++;
++ });
++ });
++ });
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "myObj");
++
++ client.awaitIdle();
++
++ myObj.mySignal(42, myObj);
++
++ compare(cb1, 1);
++ compare(cb2, 0);
++ }
++
++ function test_disconnectDuringEmit()
++ {
++ var cb1 = 0;
++ var cb2 = 0;
++ var cb3 = 0;
++ var channel = client.createChannel(function(channel) {
++ var myObj = channel.objects.myObj;
++ var cb1impl = function() {
++ cb1++;
++ };
++ myObj.mySignal.connect(cb1impl);
++ myObj.mySignal.connect(function() {
++ cb2++;
++ myObj.mySignal.disconnect(cb1impl);
++ });
++ myObj.mySignal.connect(function() {
++ cb3++;
++ });
++ });
++ client.awaitInit();
++
++ var msg = client.awaitMessage();
++ compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal);
++ compare(msg.object, "myObj");
++
++ client.awaitIdle();
++
++ myObj.mySignal(42, myObj);
++
++ compare(cb1, 1);
++ compare(cb2, 1);
++ compare(cb3, 1);
++ }
++
++ function test_overloading()
++ {
++ var signalArgs_implicit = [];
++ var signalArgs_explicit1 = [];
++ var signalArgs_explicit2 = [];
++ var signalArgs_explicit3 = [];
++ function logSignalArgs(container) {
++ return function(...args) {
++ container.push(args);
++ };
++ }
++ var returnValues = [];
++ function logReturnValue(value) {
++ returnValues.push(value);
++ }
++ var channel = client.createChannel(function(channel) {
++ var testObject = channel.objects.testObject;
++ testObject.testOverloadSignal.connect(logSignalArgs(signalArgs_implicit));
++ testObject["testOverloadSignal(int)"].connect(logSignalArgs(signalArgs_explicit1));
++ testObject["testOverloadSignal(QString)"].connect(logSignalArgs(signalArgs_explicit2));
++ testObject["testOverloadSignal(QString,int)"].connect(logSignalArgs(signalArgs_explicit3));
++ testObject.testOverload(99, logReturnValue);
++ testObject["testOverload(int)"](41, logReturnValue);
++ testObject["testOverload(QString)"]("hello world", logReturnValue);
++ testObject["testOverload(QString,int)"]("the answer is ", 41, logReturnValue);
++ });
++ client.awaitInit();
++
++ function awaitMessage(type)
++ {
++ var msg = client.awaitMessage();
++ compare(msg.type, type);
++ compare(msg.object, "testObject");
++ }
++
++ console.log("sig1");
++ awaitMessage(JSClient.QWebChannelMessageTypes.connectToSignal);
++ console.log("sig2");
++ awaitMessage(JSClient.QWebChannelMessageTypes.connectToSignal);
++ console.log("sig3");
++ awaitMessage(JSClient.QWebChannelMessageTypes.connectToSignal);
++
++ console.log("method1");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++ console.log("method2");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++ console.log("method3");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++ console.log("method4");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++
++ client.awaitIdle();
++
++ compare(signalArgs_implicit, [[99], [41]]);
++ compare(signalArgs_explicit1, signalArgs_implicit);
++ compare(signalArgs_explicit2, [["hello world"]]);
++ compare(signalArgs_explicit3, [["the answer is ", 41]]);
++ compare(returnValues, [100, 42, "HELLO WORLD", "THE ANSWER IS 42"]);
++ }
++
++ function test_variantType()
++ {
++ var returnValues = [];
++ function logReturnValue(value) {
++ returnValues.push(value);
++ }
++ var channel = client.createChannel(function(channel) {
++ var testObject = channel.objects.testObject;
++ testObject.testVariantType(0.25, logReturnValue);
++ testObject.testVariantType("0", logReturnValue);
++ testObject.testVariantType(null, logReturnValue);
++ testObject.testVariantType(testObject, logReturnValue);
++ });
++ client.awaitInit();
++
++ function awaitMessage(type)
++ {
++ var msg = client.awaitMessage();
++ compare(msg.type, type);
++ compare(msg.object, "testObject");
++ }
++
++ console.log("double arg");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++ console.log("string arg");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++ console.log("null arg");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++ console.log("QObject arg");
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++
++ client.awaitIdle();
++
++ // QMetaType::Double: 6, QMetaType::QString: 10, QMetaType::Nullptr: 51,
++ // QMetaType::QObjectStar: 39
++ compare(returnValues, [6, 10, 51, 39]);
++ }
++
++ function test_embeddedQObject()
++ {
++ var success = false;
++ function logReturnValue(value) {
++ success = value;
++ }
++ var channel = client.createChannel(function(channel) {
++ var testObject = channel.objects.testObject;
++ testObject.testEmbeddedObjects([testObject, { obj: testObject }], logReturnValue);
++ });
++ client.awaitInit();
++
++ function awaitMessage(type)
++ {
++ var msg = client.awaitMessage();
++ compare(msg.type, type);
++ compare(msg.object, "testObject");
++ }
++
++ awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod);
++
++ client.awaitIdle();
++
++ compare(success, true);
++ }
++
++ function test_customUpcaseWrapper()
++ {
++ var channel = client.createChannel(function(channel) {
++ channel.objects.testObject.stringProperty = "foo";
++ }, function(arg) { return (typeof arg === "string") ? arg.toUpperCase() : undefined });
++
++ client.awaitInit();
++ function awaitMessage(type)
++ {
++ var msg = client.awaitMessage();
++ compare(msg.type, type);
++ compare(msg.object, "testObject");
++ }
++ awaitMessage(JSClient.QWebChannelMessageTypes.setProperty);
++ compare(testObject.stringProperty, "foo"); // Don't convert in this direction
++ client.awaitIdle(); // init
++
++ testObject.stringProperty = "bar";
++ compare(testObject.stringProperty, "bar");
++ client.awaitIdle(); // property update
++ compare(channel.objects.testObject.stringProperty, "BAR"); // Case converted
++ }
++
++ function test_dateWrapper()
++ {
++ var channel = client.createChannel(undefined, "Date");
++ client.awaitInit();
++ client.awaitIdle();
++
++ var dateString = "2022-01-01T10:00:00Z";
++ myValueObj.value = dateString;
++ compare(myValueObj.value, dateString);
++ client.awaitIdle(); // property update
++ var value = channel.objects.myValueObj.value;
++ verify(value instanceof Date);
++ verify(!isNaN(value));
++ compare(value.getUTCFullYear(), 2022);
++ compare(value.getUTCMonth(), 0); // 0 = January
++ compare(value.getUTCDate(), 1);
++ compare(value.getUTCHours(), 10);
++ compare(value.getUTCMinutes(), 0);
++ compare(value.getUTCSeconds(), 0);
++
++ var invalidDate = "2022-13-31T10:00:00Z"; // Month after december
++ myValueObj.value = invalidDate;
++ compare(myValueObj.value, invalidDate);
++ client.awaitIdle(); // property update
++ value = channel.objects.myValueObj.value;
++ verify(typeof value === "string"); // Not converted to Date
++ compare(value, invalidDate);
++ }
++
++ function test_registerNull()
++ {
++ nullTest.registeredObjects = [null]
++ compare(nullTest.registeredObjects, [])
++ }
++
++ function test_noContext()
++ {
++ nullTest.registeredObjects = [testObject.createQObject()]
++ compare(nullTest.registeredObjects, [])
++ }
++ }