diff options
Diffstat (limited to 'src/qmltest/SignalSpy.qml')
-rw-r--r-- | src/qmltest/SignalSpy.qml | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/src/qmltest/SignalSpy.qml b/src/qmltest/SignalSpy.qml new file mode 100644 index 0000000000..ea77a1704d --- /dev/null +++ b/src/qmltest/SignalSpy.qml @@ -0,0 +1,252 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick 2.0 +import QtTest 1.1 + +/*! + \qmltype SignalSpy + \inqmlmodule QtTest + \brief Enables introspection of signal emission. + \since 4.8 + \ingroup qtquicktest + + In the following example, a SignalSpy is installed to watch the + "clicked" signal on a user-defined Button type. When the signal + is emitted, the \l count property on the spy will be increased. + + \code + Button { + id: button + SignalSpy { + id: spy + target: button + signalName: "clicked" + } + TestCase { + name: "ButtonClick" + function test_click() { + compare(spy.count, 0) + button.clicked(); + compare(spy.count, 1) + } + } + } + \endcode + + The above style of test is suitable for signals that are emitted + synchronously. For asynchronous signals, the wait() method can be + used to block the test until the signal occurs (or a timeout expires). + + \sa {QtTest::TestCase}{TestCase}, {Qt Quick Test} +*/ + +Item { + id: spy + visible: false + + Component.onDestruction: { + // We are potentially destroyed before the target object, + // and since only the sender (target) being destroyed destroys a connection + // in QML, and not the receiver (us/"spy"), we need to manually disconnect. + // When QTBUG-118166 is implemented, we can remove this. + let signalFunc = target ? qttest_signalFunc(target, signalName) : null + if (signalFunc) + signalFunc.disconnect(spy.qtest_activated) + } + + TestUtil { + id: util + } + // Public API. + /*! + \qmlproperty object SignalSpy::target + + This property defines the target object that will be used to + listen for emissions of the \l signalName signal. + + \sa signalName, count + */ + property var target: null + /*! + \qmlproperty string SignalSpy::signalName + + This property defines the name of the signal on \l target to + listen for. + + \sa target, count + */ + property string signalName: "" + /*! + \qmlproperty int SignalSpy::count + + This property defines the number of times that \l signalName has + been emitted from \l target since the last call to clear(). + + \sa target, signalName, clear() + \readonly + */ + readonly property alias count: spy.qtest_count + /*! + \qmlproperty bool SignalSpy::valid + + This property defines the current signal connection status. It will be true when the \l signalName of the \l target is connected successfully, otherwise it will be false. + + \sa count, target, signalName, clear() + \readonly + */ + readonly property alias valid:spy.qtest_valid + /*! + \qmlproperty list SignalSpy::signalArguments + + This property holds a list of emitted signal arguments. Each emission of the signal will append one item to the list, containing the arguments of the signal. + When connecting to a new \l target or new \l signalName or calling the \l clear() method, the \l signalArguments will be reset to empty. + + \sa signalName, clear() + \readonly + */ + readonly property alias signalArguments:spy.qtest_signalArguments + + /*! + \qmlmethod SignalSpy::clear() + + Clears \l count to 0, resets \l valid to false and clears the \l signalArguments to empty. + + \sa count, wait() + */ + function clear() { + qtest_count = 0 + qtest_expectedCount = 0 + qtest_signalArguments = [] + } + + /*! + \qmlmethod SignalSpy::wait(timeout = 5000) + + Waits for the signal \l signalName on \l target to be emitted, + for up to \a timeout milliseconds. The test case will fail if + the signal is not emitted. + + \code + SignalSpy { + id: spy + target: button + signalName: "clicked" + } + + function test_async_click() { + ... + // do something that will cause clicked() to be emitted + ... + spy.wait() + compare(spy.count, 1) + } + \endcode + + There are two possible scenarios: the signal has already been + emitted when wait() is called, or the signal has not yet been + emitted. The wait() function handles the first scenario by immediately + returning if the signal has already occurred. + + The clear() method can be used to discard information about signals + that have already occurred to synchronize wait() with future signal + emissions. + + \sa clear(), TestCase::tryCompare() + */ + function wait(timeout) { + if (timeout === undefined) + timeout = 5000 + var expected = ++qtest_expectedCount + var i = 0 + while (i < timeout && qtest_count < expected) { + qtest_results.wait(50) + i += 50 + } + var success = (qtest_count >= expected) + if (!qtest_results.verify(success, "wait for signal " + signalName, util.callerFile(), util.callerLine())) + throw new Error("QtQuickTest::fail") + } + + // Internal implementation detail follows. + + TestResult { id: qtest_results } + + onTargetChanged: { + qtest_update() + } + onSignalNameChanged: { + qtest_update() + } + + /*! \internal */ + property var qtest_prevTarget: null + /*! \internal */ + property string qtest_prevSignalName: "" + /*! \internal */ + property int qtest_expectedCount: 0 + /*! \internal */ + property var qtest_signalArguments:[] + /*! \internal */ + property int qtest_count: 0 + /*! \internal */ + property bool qtest_valid:false + /*! \internal */ + property bool qtest_reentrancy_guard: false + + /*! \internal */ + function qtest_update() { + if (qtest_reentrancy_guard) + return; + qtest_reentrancy_guard = true; + + if (qtest_prevTarget != null) { + let prevFunc = qttest_signalFunc(qtest_prevTarget, qtest_prevSignalName) + if (prevFunc) + prevFunc.disconnect(spy.qtest_activated) + qtest_prevTarget = null + qtest_prevSignalName = "" + } + if (target != null && signalName != "") { + // Look for the signal name in the object + let func = qttest_signalFunc(target, signalName) + if (func) { + qtest_prevTarget = target + qtest_prevSignalName = signalName + func.connect(spy.qtest_activated) + spy.qtest_valid = true + spy.qtest_signalArguments = [] + } else { + spy.qtest_valid = false + console.log("Signal '" + signalName + "' not found") + } + } else { + spy.qtest_valid = false + } + + qtest_reentrancy_guard = false; + } + + /*! \internal */ + function qtest_activated() { + ++qtest_count + spy.qtest_signalArguments[spy.qtest_signalArguments.length] = arguments + } + + /*! \internal */ + function qtest_signalHandlerName(sn) { + return util.signalHandlerName(sn) + } + + /*! \internal */ + function qttest_signalFunc(_target, _signalName) { + let signalFunc = _target[_signalName] + if (typeof signalFunc !== "function") { + // If it is not a function, try looking for signal handler + // i.e. (onSignal) this is needed for cases where there is a property + // and a signal with the same name, e.g. Mousearea.pressed + signalFunc = _target[qtest_signalHandlerName(_signalName)] + } + return signalFunc + } +} |