SignalSpy.qml 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. // Copyright (C) 2021 The Qt Company Ltd.
  2. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. // Qt-Security score:significant reason:default
  4. import QtQuick 2.0
  5. import QtTest 1.1
  6. /*!
  7. \qmltype SignalSpy
  8. \inqmlmodule QtTest
  9. \brief Enables introspection of signal emission.
  10. \since 4.8
  11. \ingroup qtquicktest
  12. In the following example, a SignalSpy is installed to watch the
  13. "clicked" signal on a user-defined Button type. When the signal
  14. is emitted, the \l count property on the spy will be increased.
  15. \code
  16. Button {
  17. id: button
  18. SignalSpy {
  19. id: spy
  20. target: button
  21. signalName: "clicked"
  22. }
  23. TestCase {
  24. name: "ButtonClick"
  25. function test_click() {
  26. compare(spy.count, 0)
  27. button.clicked();
  28. compare(spy.count, 1)
  29. }
  30. }
  31. }
  32. \endcode
  33. The above style of test is suitable for signals that are emitted
  34. synchronously. For asynchronous signals, the wait() method can be
  35. used to block the test until the signal occurs (or a timeout expires).
  36. \sa {QtTest::TestCase}{TestCase}, {Qt Quick Test}
  37. */
  38. Item {
  39. id: spy
  40. visible: false
  41. Component.onDestruction: {
  42. // We are potentially destroyed before the target object,
  43. // and since only the sender (target) being destroyed destroys a connection
  44. // in QML, and not the receiver (us/"spy"), we need to manually disconnect.
  45. // When QTBUG-118166 is implemented, we can remove this.
  46. let signalFunc = target ? qttest_signalFunc(target, signalName) : null
  47. if (signalFunc)
  48. signalFunc.disconnect(spy.qtest_activated)
  49. }
  50. TestUtil {
  51. id: util
  52. }
  53. // Public API.
  54. /*!
  55. \qmlproperty object SignalSpy::target
  56. This property defines the target object that will be used to
  57. listen for emissions of the \l signalName signal.
  58. \sa signalName, count
  59. */
  60. property var target: null
  61. /*!
  62. \qmlproperty string SignalSpy::signalName
  63. This property defines the name of the signal on \l target to
  64. listen for.
  65. \sa target, count
  66. */
  67. property string signalName: ""
  68. /*!
  69. \qmlproperty int SignalSpy::count
  70. This property defines the number of times that \l signalName has
  71. been emitted from \l target since the last call to clear().
  72. \sa target, signalName, clear()
  73. \readonly
  74. */
  75. readonly property alias count: spy.qtest_count
  76. /*!
  77. \qmlproperty bool SignalSpy::valid
  78. 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.
  79. \sa count, target, signalName, clear()
  80. \readonly
  81. */
  82. readonly property alias valid:spy.qtest_valid
  83. /*!
  84. \qmlproperty list SignalSpy::signalArguments
  85. 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.
  86. 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.
  87. \sa signalName, clear()
  88. \readonly
  89. */
  90. readonly property alias signalArguments:spy.qtest_signalArguments
  91. /*!
  92. \qmlmethod SignalSpy::clear()
  93. Clears \l count to \c 0 and \l signalArguments to empty. Does not affect \l valid.
  94. \sa count, wait()
  95. */
  96. function clear() {
  97. qtest_count = 0
  98. qtest_expectedCount = 0
  99. qtest_signalArguments = []
  100. }
  101. /*!
  102. \qmlmethod SignalSpy::wait(timeout = 5000)
  103. Waits for the signal \l signalName on \l target to be emitted,
  104. for up to \a timeout milliseconds. The test case will fail if
  105. the signal is not emitted.
  106. \code
  107. SignalSpy {
  108. id: spy
  109. target: button
  110. signalName: "clicked"
  111. }
  112. function test_async_click() {
  113. ...
  114. // do something that will cause clicked() to be emitted
  115. ...
  116. spy.wait()
  117. compare(spy.count, 1)
  118. }
  119. \endcode
  120. There are two possible scenarios: the signal has already been
  121. emitted when wait() is called, or the signal has not yet been
  122. emitted. The wait() function handles the first scenario by immediately
  123. returning if the signal has already occurred.
  124. The clear() method can be used to discard information about signals
  125. that have already occurred to synchronize wait() with future signal
  126. emissions.
  127. \sa clear(), TestCase::tryCompare()
  128. */
  129. function wait(timeout) {
  130. if (timeout === undefined)
  131. timeout = 5000
  132. var expected = ++qtest_expectedCount
  133. var i = 0
  134. while (i < timeout && qtest_count < expected) {
  135. qtest_results.wait(50)
  136. i += 50
  137. }
  138. var success = (qtest_count >= expected)
  139. if (!qtest_results.verify(success, "wait for signal " + signalName, util.callerFile(), util.callerLine()))
  140. throw new Error("QtQuickTest::fail")
  141. }
  142. // Internal implementation detail follows.
  143. TestResult { id: qtest_results }
  144. onTargetChanged: {
  145. qtest_update()
  146. }
  147. onSignalNameChanged: {
  148. qtest_update()
  149. }
  150. /*! \internal */
  151. property var qtest_prevTarget: null
  152. /*! \internal */
  153. property string qtest_prevSignalName: ""
  154. /*! \internal */
  155. property int qtest_expectedCount: 0
  156. /*! \internal */
  157. property var qtest_signalArguments:[]
  158. /*! \internal */
  159. property int qtest_count: 0
  160. /*! \internal */
  161. property bool qtest_valid:false
  162. /*! \internal */
  163. property bool qtest_reentrancy_guard: false
  164. /*! \internal */
  165. function qtest_update() {
  166. if (qtest_reentrancy_guard)
  167. return;
  168. qtest_reentrancy_guard = true;
  169. if (qtest_prevTarget != null) {
  170. let prevFunc = qttest_signalFunc(qtest_prevTarget, qtest_prevSignalName)
  171. if (prevFunc)
  172. prevFunc.disconnect(spy.qtest_activated)
  173. qtest_prevTarget = null
  174. qtest_prevSignalName = ""
  175. }
  176. if (target != null && signalName != "") {
  177. // Look for the signal name in the object
  178. let func = qttest_signalFunc(target, signalName)
  179. if (func) {
  180. qtest_prevTarget = target
  181. qtest_prevSignalName = signalName
  182. func.connect(spy.qtest_activated)
  183. spy.qtest_valid = true
  184. spy.qtest_signalArguments = []
  185. } else {
  186. spy.qtest_valid = false
  187. console.log("Signal '" + signalName + "' not found")
  188. }
  189. } else {
  190. spy.qtest_valid = false
  191. }
  192. qtest_reentrancy_guard = false;
  193. }
  194. /*! \internal */
  195. function qtest_activated() {
  196. ++qtest_count
  197. spy.qtest_signalArguments[spy.qtest_signalArguments.length] = arguments
  198. }
  199. /*! \internal */
  200. function qtest_signalHandlerName(sn) {
  201. return util.signalHandlerName(sn)
  202. }
  203. /*! \internal */
  204. function qttest_signalFunc(_target, _signalName) {
  205. let signalFunc = _target[_signalName]
  206. if (typeof signalFunc !== "function") {
  207. // If it is not a function, try looking for signal handler
  208. // i.e. (onSignal) this is needed for cases where there is a property
  209. // and a signal with the same name, e.g. Mousearea.pressed
  210. signalFunc = _target[qtest_signalHandlerName(_signalName)]
  211. }
  212. return signalFunc
  213. }
  214. }