diff options
Diffstat (limited to 'src/qmltest/TestCase.qml')
-rw-r--r-- | src/qmltest/TestCase.qml | 2202 |
1 files changed, 2202 insertions, 0 deletions
diff --git a/src/qmltest/TestCase.qml b/src/qmltest/TestCase.qml new file mode 100644 index 0000000000..9dac6ae249 --- /dev/null +++ b/src/qmltest/TestCase.qml @@ -0,0 +1,2202 @@ +// Copyright (C) 2016 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 QtQuick.Window 2.0 // used for qtest_verifyItem +import QtTest 1.2 +import "testlogger.js" as TestLogger + +/*! + \qmltype TestCase + \inqmlmodule QtTest + \brief Represents a unit test case. + \since 4.8 + \ingroup qtquicktest + + \section1 Introduction to QML Test Cases + + Test cases are written as JavaScript functions within a TestCase + type: + + \code + import QtQuick 2.0 + import QtTest 1.2 + + TestCase { + name: "MathTests" + + function test_math() { + compare(2 + 2, 4, "2 + 2 = 4") + } + + function test_fail() { + compare(2 + 2, 5, "2 + 2 = 5") + } + } + \endcode + + Functions whose names start with "test_" are treated as test cases + to be executed. The \l name property is used to prefix the functions + in the output: + + \code + ********* Start testing of MathTests ********* + Config: Using QTest library 4.7.2, Qt 4.7.2 + PASS : MathTests::initTestCase() + FAIL! : MathTests::test_fail() 2 + 2 = 5 + Actual (): 4 + Expected (): 5 + Loc: [/home/.../tst_math.qml(12)] + PASS : MathTests::test_math() + PASS : MathTests::cleanupTestCase() + Totals: 3 passed, 1 failed, 0 skipped + ********* Finished testing of MathTests ********* + \endcode + + Because of the way JavaScript properties work, the order in which the + test functions are found is unpredictable. To assist with predictability, + the test framework will sort the functions on ascending order of name. + This can help when there are two tests that must be run in order. + + Multiple TestCase types can be supplied. The test program will exit + once they have all completed. If a test case doesn't need to run + (because a precondition has failed), then \l optional can be set to true. + + \section1 Data-driven Tests + + Table data can be provided to a test using a function name that ends + with "_data". Alternatively, the \c init_data() function can be used + to provide default test data for all test functions without a matching + "_data" function in a TestCase type: + + + \code + import QtQuick 2.0 + import QtTest 1.2 + + TestCase { + name: "DataTests" + + function init_data() { + return [ + {tag:"init_data_1", a:1, b:2, answer: 3}, + {tag:"init_data_2", a:2, b:4, answer: 6} + ]; + } + + function test_table_data() { + return [ + {tag: "2 + 2 = 4", a: 2, b: 2, answer: 4 }, + {tag: "2 + 6 = 8", a: 2, b: 6, answer: 8 }, + ] + } + + function test_table(data) { + //data comes from test_table_data + compare(data.a + data.b, data.answer) + } + + function test_default_table(data) { + //data comes from init_data + compare(data.a + data.b, data.answer) + } + } + \endcode + + The test framework will iterate over all of the rows in the table + and pass each row to the test function. As shown, the columns can be + extracted for use in the test. The \c tag column is special - it is + printed by the test framework when a row fails, to help the reader + identify which case failed amongst a set of otherwise passing tests. + + \section1 Benchmarks + + Functions whose names start with "benchmark_" will be run multiple + times with the Qt benchmark framework, with an average timing value + reported for the runs. This is equivalent to using the \c{QBENCHMARK} + macro in the C++ version of QTestLib. + + \code + TestCase { + id: top + name: "CreateBenchmark" + + function benchmark_create_component() { + let component = Qt.createComponent("item.qml") + let obj = component.createObject(top) + obj.destroy() + component.destroy() + } + } + + RESULT : CreateBenchmark::benchmark_create_component: + 0.23 msecs per iteration (total: 60, iterations: 256) + PASS : CreateBenchmark::benchmark_create_component() + \endcode + + To get the effect of the \c{QBENCHMARK_ONCE} macro, prefix the test + function name with "benchmark_once_". + + \section1 Simulating Keyboard and Mouse Events + + The keyPress(), keyRelease(), and keyClick() methods can be used + to simulate keyboard events within unit tests. The events are + delivered to the currently focused QML item. You can pass either + a Qt.Key enum value or a latin1 char (string of length one) + + \code + Rectangle { + width: 50; height: 50 + focus: true + + TestCase { + name: "KeyClick" + when: windowShown + + function test_key_click() { + keyClick(Qt.Key_Left) + keyClick("a") + ... + } + } + } + \endcode + + The mousePress(), mouseRelease(), mouseClick(), mouseDoubleClickSequence() + and mouseMove() methods can be used to simulate mouse events in a + similar fashion. + + If your test creates other windows, it's possible that those windows + become active, stealing the focus from the TestCase's window. To ensure + that the TestCase's window is active, use the following code: + + \code + testCase.Window.window.requestActivate() + tryCompare(testCase.Window.window, "active", true) + \endcode + + \b{Note:} keyboard and mouse events can only be delivered once the + main window has been shown. Attempts to deliver events before then + will fail. Use the \l when and windowShown properties to track + when the main window has been shown. + + \section1 Managing Dynamically Created Test Objects + + A typical pattern with QML tests is to + \l {Dynamic QML Object Creation from JavaScript}{dynamically create} + an item and then destroy it at the end of the test function: + + \code + TestCase { + id: testCase + name: "MyTest" + when: windowShown + + function test_click() { + let item = Qt.createQmlObject("import QtQuick 2.0; Item {}", testCase); + verify(item); + + // Test item... + + item.destroy(); + } + } + \endcode + + The problem with this pattern is that any failures in the test function + will cause the call to \c item.destroy() to be skipped, leaving the item + hanging around in the scene until the test case has finished. This can + result in interference with future tests; for example, by blocking input + events or producing unrelated debug output that makes it difficult to + follow the code's execution. + + By calling \l createTemporaryQmlObject() instead, the object is guaranteed + to be destroyed at the end of the test function: + + \code + TestCase { + id: testCase + name: "MyTest" + when: windowShown + + function test_click() { + let item = createTemporaryQmlObject("import QtQuick 2.0; Item {}", testCase); + verify(item); + + // Test item... + + // Don't need to worry about destroying "item" here. + } + } + \endcode + + For objects that are created via the \l {Component::}{createObject()} function + of \l Component, the \l createTemporaryObject() function can be used. + + \sa {QtTest::SignalSpy}{SignalSpy}, {Qt Quick Test} + + \section1 Separating Tests from Application Logic + + In most cases, you would want to separate your tests from the application + logic by splitting them into different projects and linking them. + + For example, you could have the following project structure: + + \badcode + . + | — CMakeLists.txt + | - main.qml + | — src + | — main.cpp + | — MyModule + | — MyButton.qml + | — CMakeLists.txt + | — tests + | — tst_testqml.qml + | — main.cpp + | — setup.cpp + | — setup.h + \endcode + + Now, to test \c MyModule/MyButton.qml, create a library for + \c MyModule in \c MyModule/CMakeLists.txt and link it to your + test project, \c tests/UnitQMLTests/CMakeLists.txt: + + \if defined(onlinedocs) + \tab {build-qt-app}{tab-cmake-add-library}{MyModule/CMakeLists.txt}{checked} + \tab {build-qt-app}{tab-cmake-link-against-library}{tests/CMakeLists.txt}{} + \tab {build-qt-app}{tab-tests_main}{tests/main.cpp}{} + \tab {build-qt-app}{tab-tests-setup-cpp}{tests/setup.cpp}{} + \tab + {build-qt-app}{tab-tests-setup-h}{tests/setup.h}{} + \tab {build-qt-app}{tab-project-cmake}{CMakeLists.txt}{} + \tabcontent {tab-cmake-add-library} + \else + \section2 Add Library + \endif + \dots + \snippet testApp/MyModule/CMakeLists.txt add library + \dots + \if defined(onlinedocs) + \endtabcontent + \tabcontent {tab-cmake-link-against-library} + \else + \section2 Link Against Library + \endif + \dots + \snippet testApp/tests/CMakeLists.txt link against library + \dots + \if defined(onlinedocs) + \endtabcontent + \tabcontent {tab-tests_main} + \else + \section2 Test main.cpp + \endif + \snippet testApp/tests/main.cpp main + \if defined(onlinedocs) + \endtabcontent + \tabcontent {tab-tests-setup-cpp} + \else + \section2 Test Setup C++ + \endif + \snippet testApp/tests/setup.cpp setup + \if defined(onlinedocs) + \endtabcontent + \tabcontent {tab-tests-setup-h} + \else + \section2 Test Setup Header + \endif + \snippet testApp/tests/setup.h setup + \if defined(onlinedocs) + \endtabcontent + \tabcontent {tab-project-cmake} + \else + \section2 Project CMakeLists + \endif + \dots + \snippet testApp/CMakeLists.txt project-cmake + \dots + \if defined(onlinedocs) + \endtabcontent + \endif + + + Then, in \c tests/tst_testqml.qml, you can import + \c MyModule/MyButton.qml: + + \if defined(onlinedocs) + \tab {test-qml}{tab-qml-import}{tests/tst_testqml.qml}{checked} + \tab {test-qml}{tab-qml-my-button}{MyModule/MyButton.qml}{} + \tabcontent {tab-qml-import} + \else + \section2 Import QML + \endif + \snippet testApp/tests/tst_testqml.qml import + \if defined(onlinedocs) + \endtabcontent + \tabcontent {tab-qml-my-button} + \else + \section2 Define QML Button + \endif + \snippet testApp/MyModule/MyButton.qml define + \if defined(onlinedocs) + \endtabcontent + \endif +*/ + + +Item { + id: testCase + visible: false + TestUtil { + id:util + } + + /*! + \qmlproperty string TestCase::name + + This property defines the name of the test case for result reporting. + The default value is an empty string. + + \code + TestCase { + name: "ButtonTests" + ... + } + \endcode + */ + property string name + + /*! + \qmlproperty bool TestCase::when + + This property should be set to true when the application wants + the test cases to run. The default value is true. In the following + example, a test is run when the user presses the mouse button: + + \code + Rectangle { + id: foo + width: 640; height: 480 + color: "cyan" + + MouseArea { + id: area + anchors.fill: parent + } + + property bool bar: true + + TestCase { + name: "ItemTests" + when: area.pressed + id: test1 + + function test_bar() { + verify(bar) + } + } + } + \endcode + + The test application will exit once all \l TestCase types + have been triggered and have run. The \l optional property can + be used to exclude a \l TestCase type. + + \sa optional, completed + */ + property bool when: true + + /*! + \qmlproperty bool TestCase::completed + + This property will be set to true once the test case has completed + execution. Test cases are only executed once. The initial value + is false. + + \sa running, when + */ + property bool completed: false + + /*! + \qmlproperty bool TestCase::running + + This property will be set to true while the test case is running. + The initial value is false, and the value will become false again + once the test case completes. + + \sa completed, when + */ + property bool running: false + + /*! + \qmlproperty bool TestCase::optional + + Multiple \l TestCase types can be supplied in a test application. + The application will exit once they have all completed. If a test case + does not need to run (because a precondition has failed), then this + property can be set to true. The default value is false. + + \code + TestCase { + when: false + optional: true + function test_not_run() { + verify(false) + } + } + \endcode + + \sa when, completed + */ + property bool optional: false + + /*! + \qmlproperty bool TestCase::windowShown + + This property will be set to true after the QML viewing window has + been displayed. Normally test cases run as soon as the test application + is loaded and before a window is displayed. If the test case involves + visual types and behaviors, then it may need to be delayed until + after the window is shown. + + \code + Button { + id: button + onClicked: text = "Clicked" + TestCase { + name: "ClickTest" + when: windowShown + function test_click() { + button.clicked(); + compare(button.text, "Clicked"); + } + } + } + \endcode + */ + property bool windowShown: QTestRootObject.windowShown + + // Internal private state. Identifiers prefixed with qtest are reserved. + /*! \internal */ + property bool qtest_prevWhen: true + /*! \internal */ + property int qtest_testId: -1 + /*! \internal */ + property bool qtest_componentCompleted : false + /*! \internal */ + property var qtest_testCaseResult + /*! \internal */ + property var qtest_results: qtest_results_normal + /*! \internal */ + TestResult { id: qtest_results_normal } + /*! \internal */ + property var qtest_events: qtest_events_normal + TestEvent { id: qtest_events_normal } + /*! \internal */ + property var qtest_temporaryObjects: [] + + /*! + \qmlmethod TestCase::fail(message = "") + + Fails the current test case, with the optional \a message. + Similar to \c{QFAIL(message)} in C++. + */ + function fail(msg) { + if (msg === undefined) + msg = ""; + qtest_results.fail(msg, util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + /*! \internal */ + function qtest_fail(msg, frame) { + if (msg === undefined) + msg = ""; + qtest_results.fail(msg, util.callerFile(frame), util.callerLine(frame)) + throw new Error("QtQuickTest::fail") + } + + /*! + \qmlmethod TestCase::verify(condition, message = "") + + Fails the current test case if \a condition is false, and + displays the optional \a message. Similar to \c{QVERIFY(condition)} + or \c{QVERIFY2(condition, message)} in C++. + */ + function verify(cond, msg, ...args) { + if (args.length > 0) + qtest_fail("More than two arguments given to verify(). Did you mean tryVerify() or tryCompare()?", 1) + + if (msg === undefined) + msg = ""; + if (!qtest_results.verify(cond, msg, util.callerFile(), util.callerLine())) + throw new Error("QtQuickTest::fail") + } + + /*! + \since 5.8 + \qmlmethod TestCase::tryVerify(function, timeout = 5000, message = "") + + Fails the current test case if \a function does not evaluate to + \c true before the specified \a timeout (in milliseconds) has elapsed. + The function is evaluated multiple times until the timeout is + reached. An optional \a message is displayed upon failure. + + This function is intended for testing applications where a condition + changes based on asynchronous events. Use verify() for testing + synchronous condition changes, and tryCompare() for testing + asynchronous property changes. + + For example, in the code below, it's not possible to use tryCompare(), + because the \c currentItem property might be \c null for a short period + of time: + + \code + tryCompare(listView.currentItem, "text", "Hello"); + \endcode + + Instead, we can use tryVerify() to first check that \c currentItem + isn't \c null, and then use a regular compare afterwards: + + \code + tryVerify(function(){ return listView.currentItem }) + compare(listView.currentItem.text, "Hello") + \endcode + + \sa verify(), compare(), tryCompare(), SignalSpy::wait() + */ + function tryVerify(expressionFunction, timeout, msg) { + if (!expressionFunction || !(expressionFunction instanceof Function)) { + qtest_results.fail("First argument must be a function", util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + if (timeout && typeof(timeout) !== "number") { + qtest_results.fail("timeout argument must be a number", util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + if (msg && typeof(msg) !== "string") { + qtest_results.fail("message argument must be a string", util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + if (!timeout) + timeout = 5000 + + if (msg === undefined) + msg = "function returned false" + + if (!expressionFunction()) + wait(0) + + let i = 0 + while (i < timeout && !expressionFunction()) { + wait(50) + i += 50 + } + + if (!qtest_results.verify(expressionFunction(), msg, util.callerFile(), util.callerLine())) + throw new Error("QtQuickTest::fail") + } + + /*! + \since 5.13 + \qmlmethod bool TestCase::isPolishScheduled(object itemOrWindow) + + If \a itemOrWindow is an \l Item, this function returns \c true if + \l {QQuickItem::}{updatePolish()} has not been called on it since the + last call to \l {QQuickItem::}{polish()}, otherwise returns \c false. + + Since Qt 6.5, if \a itemOrWindow is a \l Window, this function returns + \c true if \l {QQuickItem::}{updatePolish()} has not been called on any + item it manages since the last call to \l {QQuickItem::}{polish()} on + those items, otherwise returns \c false. + + When assigning values to properties in QML, any layouting the item + must do as a result of the assignment might not take effect immediately, + but can instead be postponed until the item is polished. For these cases, + you can use this function to ensure that items have been polished + before the execution of the test continues. For example: + + \code + verify(isPolishScheduled(item)) + verify(waitForItemPolished(item)) + \endcode + + Without the call to \c isPolishScheduled() above, the + call to \c waitForItemPolished() might see that no polish + was scheduled and therefore pass instantly, assuming that + the item had already been polished. This function + makes it obvious why an item wasn't polished and allows tests to + fail early under such circumstances. + + \sa waitForPolish(), QQuickItem::polish(), QQuickItem::updatePolish() + */ + function isPolishScheduled(itemOrWindow) { + if (!itemOrWindow || typeof itemOrWindow !== "object") { + qtest_results.fail("Argument must be a valid Item or Window; actual type is " + typeof itemOrWindow, + util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + return qtest_results.isPolishScheduled(itemOrWindow) + } + + /*! + \since 5.13 + \deprecated [6.5] Use \l waitForPolish() instead. + \qmlmethod bool waitForItemPolished(object item, int timeout = 5000) + + Waits for \a timeout milliseconds or until + \l {QQuickItem::}{updatePolish()} has been called on \a item. + + Returns \c true if \c updatePolish() was called on \a item within + \a timeout milliseconds, otherwise returns \c false. + + \sa isPolishScheduled(), QQuickItem::polish(), QQuickItem::updatePolish() + */ + function waitForItemPolished(item, timeout) { + return waitForPolish(item, timeout) + } + + /*! + \since 6.5 + \qmlmethod bool waitForPolish(object windowOrItem, int timeout = 5000) + + If \a windowOrItem is an Item, this functions waits for \a timeout + milliseconds or until \c isPolishScheduled(windowOrItem) returns \c false. + Returns \c true if \c isPolishScheduled(windowOrItem) returns \c false within + \a timeout milliseconds, otherwise returns \c false. + + If \c windowOrItem is a Window, this functions waits for \c timeout + milliseconds or until \c isPolishScheduled() returns \c false for + all items managed by the window. Returns \c true if + \c isPolishScheduled() returns \c false for all items within + \a timeout milliseconds, otherwise returns \c false. + + \sa isPolishScheduled(), QQuickItem::polish(), QQuickItem::updatePolish() + */ + function waitForPolish(windowOrItem, timeout) { + if (!windowOrItem || typeof windowOrItem !== "object") { + qtest_results.fail("First argument must be a valid Item or Window; actual type is " + typeof windowOrItem, + util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + if (timeout !== undefined && typeof(timeout) !== "number") { + qtest_results.fail("Second argument must be a number; actual type is " + typeof timeout, + util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + + if (!timeout) + timeout = 5000 + + return qtest_results.waitForPolish(windowOrItem, timeout) + } + + /*! + \since 5.9 + \qmlmethod object TestCase::createTemporaryQmlObject(string qml, object parent, string filePath) + + This function dynamically creates a QML object from the given \a qml + string with the specified \a parent. The returned object will be + destroyed (if it was not already) after \l cleanup() has finished + executing, meaning that objects created with this function are + guaranteed to be destroyed after each test, regardless of whether or + not the tests fail. + + If there was an error while creating the object, \c null will be + returned. + + If \a filePath is specified, it will be used for error reporting for + the created object. + + This function calls + \l {QtQml::Qt::createQmlObject()}{Qt.createQmlObject()} internally. + + \sa {Managing Dynamically Created Test Objects} + */ + function createTemporaryQmlObject(qml, parent, filePath) { + if (typeof qml !== "string") { + qtest_results.fail("First argument must be a string of QML; actual type is " + typeof qml, + util.callerFile(), util.callerLine()); + throw new Error("QtQuickTest::fail"); + } + + if (!parent || typeof parent !== "object") { + qtest_results.fail("Second argument must be a valid parent object; actual type is " + typeof parent, + util.callerFile(), util.callerLine()); + throw new Error("QtQuickTest::fail"); + } + + if (filePath !== undefined && typeof filePath !== "string") { + qtest_results.fail("Third argument must be a file path string; actual type is " + typeof filePath, + util.callerFile(), util.callerLine()); + throw new Error("QtQuickTest::fail"); + } + + let object = Qt.createQmlObject(qml, parent, filePath); + qtest_temporaryObjects.push(object); + return object; + } + + /*! + \since 5.9 + \qmlmethod object TestCase::createTemporaryObject(Component component, object parent, object properties) + + This function dynamically creates a QML object from the given + \a component with the specified optional \a parent and \a properties. + The returned object will be destroyed (if it was not already) after + \l cleanup() has finished executing, meaning that objects created with + this function are guaranteed to be destroyed after each test, + regardless of whether or not the tests fail. + + If there was an error while creating the object, \c null will be + returned. + + This function calls + \l {QtQml::Component::createObject()}{component.createObject()} + internally. + + \sa {Managing Dynamically Created Test Objects} + */ + function createTemporaryObject(component, parent, properties) { + if (typeof component !== "object") { + qtest_results.fail("First argument must be a Component; actual type is " + typeof component, + util.callerFile(), util.callerLine()); + throw new Error("QtQuickTest::fail"); + } + + if (properties && typeof properties !== "object") { + qtest_results.fail("Third argument must be an object; actual type is " + typeof properties, + util.callerFile(), util.callerLine()); + throw new Error("QtQuickTest::fail"); + } + + if (parent === undefined) + parent = null + + let object = component.createObject(parent, properties ? properties : ({})); + qtest_temporaryObjects.push(object); + return object; + } + + /*! + \internal + + Destroys all temporary objects that still exist. + */ + function qtest_destroyTemporaryObjects() { + for (let i = 0; i < qtest_temporaryObjects.length; ++i) { + let temporaryObject = qtest_temporaryObjects[i]; + // ### the typeof check can be removed when QTBUG-57749 is fixed + if (temporaryObject && typeof temporaryObject.destroy === "function") + temporaryObject.destroy(); + } + qtest_temporaryObjects = []; + } + + /*! \internal */ + // Determine what is o. + // Discussions and reference: http://philrathe.com/articles/equiv + // Test suites: http://philrathe.com/tests/equiv + // Author: Philippe Rathé <prathe@gmail.com> + function qtest_typeof(o) { + if (typeof o === "undefined") { + return "undefined"; + + // consider: typeof null === object + } else if (o === null) { + return "null"; + + } else if (o.constructor === String) { + return "string"; + + } else if (o.constructor === Boolean) { + return "boolean"; + + } else if (o.constructor === Number) { + + if (isNaN(o)) { + return "nan"; + } else { + return "number"; + } + // consider: typeof [] === object + } else if (o instanceof Array) { + return "array"; + + // consider: typeof new Date() === object + } else if (o instanceof Date) { + return "date"; + + // consider: /./ instanceof Object; + // /./ instanceof RegExp; + // typeof /./ === "function"; // => false in IE and Opera, + // true in FF and Safari + } else if (o instanceof RegExp) { + return "regexp"; + + } else if (typeof o === "object") { + if ("mapFromItem" in o && "mapToItem" in o) { + return "declarativeitem"; // @todo improve detection of declarative items + } else if ("x" in o && "y" in o && "z" in o) { + return "vector3d"; // Qt 3D vector + } + return "object"; + } else if (o instanceof Function) { + return "function"; + } else { + return undefined; + } + } + + /*! \internal */ + // Test for equality + // Large parts contain sources from QUnit or http://philrathe.com + // Discussions and reference: http://philrathe.com/articles/equiv + // Test suites: http://philrathe.com/tests/equiv + // Author: Philippe Rathé <prathe@gmail.com> + function qtest_compareInternal(act, exp) { + let success = false; + if (act === exp) { + success = true; // catch the most you can + } else if (act === null || exp === null || typeof act === "undefined" || typeof exp === "undefined") { + success = false; // don't lose time with error prone cases + } else { + let typeExp = qtest_typeof(exp), typeAct = qtest_typeof(act) + if (typeExp !== typeAct) { + // allow object vs string comparison (e.g. for colors) + // else break on different types + if ((typeExp === "string" && (typeAct === "object") || typeAct === "declarativeitem") + || ((typeExp === "object" || typeExp === "declarativeitem") && typeAct === "string")) { + success = (act == exp) // @disable-check M126 + } + } else if (typeExp === "string" || typeExp === "boolean" || + typeExp === "null" || typeExp === "undefined") { + if (exp instanceof act.constructor || act instanceof exp.constructor) { + // to catch short annotaion VS 'new' annotation of act declaration + // e.g. let i = 1; + // let j = new Number(1); + success = (act == exp) // @disable-check M126 + } else { + success = (act === exp) + } + } else if (typeExp === "nan") { + success = isNaN(act); + } else if (typeExp === "number") { + // Use act fuzzy compare if the two values are floats + if (Math.abs(act - exp) <= 0.00001) { + success = true + } + } else if (typeExp === "array") { + success = qtest_compareInternalArrays(act, exp) + } else if (typeExp === "object") { + success = qtest_compareInternalObjects(act, exp) + } else if (typeExp === "declarativeitem") { + success = qtest_compareInternalObjects(act, exp) // @todo improve comparison of declarative items + } else if (typeExp === "vector3d") { + success = (Math.abs(act.x - exp.x) <= 0.00001 && + Math.abs(act.y - exp.y) <= 0.00001 && + Math.abs(act.z - exp.z) <= 0.00001) + } else if (typeExp === "date") { + success = (act.valueOf() === exp.valueOf()) + } else if (typeExp === "regexp") { + success = (act.source === exp.source && // the regex itself + act.global === exp.global && // and its modifers (gmi) ... + act.ignoreCase === exp.ignoreCase && + act.multiline === exp.multiline) + } + } + return success + } + + /*! \internal */ + function qtest_compareInternalObjects(act, exp) { + let i; + let eq = true; // unless we can proove it + let aProperties = [], bProperties = []; // collection of strings + + // comparing constructors is more strict than using instanceof + if (act.constructor !== exp.constructor) { + return false; + } + + for (i in act) { // be strict: don't ensures hasOwnProperty and go deep + aProperties.push(i); // collect act's properties + if (!qtest_compareInternal(act[i], exp[i])) { + eq = false; + break; + } + } + + for (i in exp) { + bProperties.push(i); // collect exp's properties + } + + if (aProperties.length === 0 && bProperties.length === 0) { // at least a special case for QUrl + return eq && (JSON.stringify(act) === JSON.stringify(exp)); + } + + // Ensures identical properties name + return eq && qtest_compareInternal(aProperties.sort(), bProperties.sort()); + + } + + /*! \internal */ + function qtest_compareInternalArrays(actual, expected) { + if (actual.length !== expected.length) { + return false + } + + for (let i = 0, len = actual.length; i < len; i++) { + if (!qtest_compareInternal(actual[i], expected[i])) { + return false + } + } + + return true + } + + /*! + \qmlmethod TestCase::compare(actual, expected, message = "") + + Fails the current test case if \a actual is not the same as + \a expected, and displays the optional \a message. Similar + to \c{QCOMPARE(actual, expected)} in C++. + + \sa tryCompare(), fuzzyCompare + */ + function compare(actual, expected, msg) { + let act = qtest_results.stringify(actual) + let exp = qtest_results.stringify(expected) + + let success = qtest_compareInternal(actual, expected) + if (msg === undefined) { + if (success) + msg = "COMPARE()" + else + msg = "Compared values are not the same" + } + if (!qtest_results.compare(success, msg, act, exp, util.callerFile(), util.callerLine())) { + throw new Error("QtQuickTest::fail") + } + } + + /*! + \qmlmethod TestCase::fuzzyCompare(actual, expected, delta, message = "") + + Fails the current test case if the difference betwen \a actual and \a expected + is greater than \a delta, and displays the optional \a message. Similar + to \c{qFuzzyCompare(actual, expected)} in C++ but with a required \a delta value. + + This function can also be used for color comparisons if both the \a actual and + \a expected values can be converted into color values. If any of the differences + for RGBA channel values are greater than \a delta, the test fails. + + \sa tryCompare(), compare() + */ + function fuzzyCompare(actual, expected, delta, msg) { + if (delta === undefined) + qtest_fail("A delta value is required for fuzzyCompare", 2) + + let success = qtest_results.fuzzyCompare(actual, expected, delta) + if (msg === undefined) { + if (success) + msg = "FUZZYCOMPARE()" + else + msg = "Compared values are not the same with delta(" + delta + ")" + } + + if (!qtest_results.compare(success, msg, actual, expected, util.callerFile(), util.callerLine())) { + throw new Error("QtQuickTest::fail") + } + } + + /*! + \qmlmethod object TestCase::grabImage(item) + + Returns a snapshot image object of the given \a item. + + The returned image object has the following properties: + \list + \li width Returns the width of the underlying image (since 5.10) + \li height Returns the height of the underlying image (since 5.10) + \li size Returns the size of the underlying image (since 5.10) + \endlist + + Additionally, the returned image object has the following methods: + \list + \li \c {red(x, y)} Returns the red channel value of the pixel at \e x, \e y position + \li \c {green(x, y)} Returns the green channel value of the pixel at \e x, \e y position + \li \c {blue(x, y)} Returns the blue channel value of the pixel at \e x, \e y position + \li \c {alpha(x, y)} Returns the alpha channel value of the pixel at \e x, \e y position + \li \c {pixel(x, y)} Returns the color value of the pixel at \e x, \e y position + \li \c {equals(image)} Returns \c true if this image is identical to \e image - + see \l QImage::operator== (since 5.6) + + For example: + + \code + let image = grabImage(rect); + compare(image.red(10, 10), 255); + compare(image.pixel(20, 20), Qt.rgba(255, 0, 0, 255)); + + rect.width += 10; + let newImage = grabImage(rect); + verify(!newImage.equals(image)); + \endcode + + \li \c {save(path)} Saves the image to the given \e path. If the image cannot + be saved, an exception will be thrown. (since 5.10) + + This can be useful to perform postmortem analysis on failing tests, for + example: + + \code + let image = grabImage(rect); + try { + compare(image.width, 100); + } catch (ex) { + image.save("debug.png"); + throw ex; + } + \endcode + + \endlist + */ + function grabImage(item) { + return qtest_results.grabImage(item); + } + + /*! + \since 5.4 + \qmlmethod QtObject TestCase::findChild(parent, objectName) + + Returns the first child of \a parent with \a objectName, or \c null if + no such item exists. Both visual and non-visual children are searched + recursively, with visual children being searched first. + + \code + compare(findChild(item, "childObject"), expectedChildObject); + \endcode + */ + function findChild(parent, objectName) { + // First, search the visual item hierarchy. + let child = qtest_findVisualChild(parent, objectName); + if (child) + return child; + + // If it's not a visual child, it might be a QObject child. + return qtest_results.findChild(parent, objectName); + } + + /*! \internal */ + function qtest_findVisualChild(parent, objectName) { + if (!parent || parent.children === undefined) + return null; + + for (let i = 0; i < parent.children.length; ++i) { + // Is this direct child of ours the child we're after? + let child = parent.children[i]; + if (child.objectName === objectName) + return child; + } + + for (let i = 0; i < parent.children.length; ++i) { + // Try the direct child's children. + let child = qtest_findVisualChild(parent.children[i], objectName); + if (child) + return child; + } + return null; + } + + /*! + \qmlmethod TestCase::tryCompare(obj, property, expected, timeout = 5000, message = "") + + Fails the current test case if the specified \a property on \a obj + is not the same as \a expected, and displays the optional \a message. + The test will be retried multiple times until the + \a timeout (in milliseconds) is reached. + + This function is intended for testing applications where a property + changes value based on asynchronous events. Use compare() for testing + synchronous property changes. + + \code + tryCompare(img, "status", BorderImage.Ready) + compare(img.width, 120) + compare(img.height, 120) + compare(img.horizontalTileMode, BorderImage.Stretch) + compare(img.verticalTileMode, BorderImage.Stretch) + \endcode + + SignalSpy::wait() provides an alternative method to wait for a + signal to be emitted. + + \sa compare(), SignalSpy::wait() + */ + function tryCompare(obj, prop, ...args) { + if (typeof(prop) !== "string" && typeof(prop) !== "number") { + qtest_results.fail("A property name as string or index is required for tryCompare", + util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + if (args.length === 0) { + qtest_results.fail("A value is required for tryCompare", + util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + let [value, timeout, msg] = args + if (timeout !== undefined && typeof(timeout) !== "number") { + qtest_results.fail("timeout should be a number", + util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::fail") + } + if (!timeout) + timeout = 5000 + if (msg === undefined) + msg = "property " + prop + if (!qtest_compareInternal(obj[prop], value)) + wait(0) + let i = 0 + while (i < timeout && !qtest_compareInternal(obj[prop], value)) { + wait(50) + i += 50 + } + let actual = obj[prop] + let act = qtest_results.stringify(actual) + let exp = qtest_results.stringify(value) + let success = qtest_compareInternal(actual, value) + if (!qtest_results.compare(success, msg, act, exp, util.callerFile(), util.callerLine())) + throw new Error("QtQuickTest::fail") + } + + /*! + \qmlmethod TestCase::skip(message = "") + + Skips the current test case and prints the optional \a message. + If this is a data-driven test, then only the current row is skipped. + Similar to \c{QSKIP(message)} in C++. + */ + function skip(msg) { + if (msg === undefined) + msg = "" + qtest_results.skip(msg, util.callerFile(), util.callerLine()) + throw new Error("QtQuickTest::skip") + } + + /*! + \qmlmethod TestCase::expectFail(tag, message) + + In a data-driven test, marks the row associated with \a tag as + expected to fail. When the fail occurs, display the \a message, + abort the test, and mark the test as passing. Similar to + \c{QEXPECT_FAIL(tag, message, Abort)} in C++. + + If the test is not data-driven, then \a tag must be set to + an empty string. + + \sa expectFailContinue() + */ + function expectFail(tag, msg) { + if (tag === undefined) { + warn("tag argument missing from expectFail()") + tag = "" + } + if (msg === undefined) { + warn("message argument missing from expectFail()") + msg = "" + } + if (!qtest_results.expectFail(tag, msg, util.callerFile(), util.callerLine())) + throw new Error("QtQuickTest::expectFail") + } + + /*! + \qmlmethod TestCase::expectFailContinue(tag, message) + + In a data-driven test, marks the row associated with \a tag as + expected to fail. When the fail occurs, display the \a message, + and then continue the test. Similar to + \c{QEXPECT_FAIL(tag, message, Continue)} in C++. + + If the test is not data-driven, then \a tag must be set to + an empty string. + + \sa expectFail() + */ + function expectFailContinue(tag, msg) { + if (tag === undefined) { + warn("tag argument missing from expectFailContinue()") + tag = "" + } + if (msg === undefined) { + warn("message argument missing from expectFailContinue()") + msg = "" + } + if (!qtest_results.expectFailContinue(tag, msg, util.callerFile(), util.callerLine())) + throw new Error("QtQuickTest::expectFail") + } + + /*! + \qmlmethod TestCase::warn(message) + + Prints \a message as a warning message. Similar to + \c{qWarning(message)} in C++. + + \sa ignoreWarning() + */ + function warn(msg) { + if (msg === undefined) + msg = "" + qtest_results.warn(msg, util.callerFile(), util.callerLine()); + } + + /*! + \qmlmethod TestCase::ignoreWarning(message) + + Marks \a message as an ignored warning message. When it occurs, + the warning will not be printed and the test passes. If the message + does not occur, then the test will fail. Similar to + \c{QTest::ignoreMessage(QtWarningMsg, message)} in C++. + + Since Qt 5.12, \a message can be either a string, or a regular + expression providing a pattern of messages to ignore. + + For example, the following snippet will ignore a string warning message: + \qml + ignoreWarning("Something sort of bad happened") + \endqml + + And the following snippet will ignore a regular expression matching a + number of possible warning messages: + \qml + ignoreWarning(new RegExp("[0-9]+ bad things happened")) + \endqml + + \note Despite being a JavaScript RegExp object, it will not be + interpreted as such; instead, the pattern will be passed to + \l QRegularExpression. + + \sa warn() + */ + function ignoreWarning(msg) { + if (msg === undefined) + msg = "" + qtest_results.ignoreWarning(msg) + } + + /*! + \qmlmethod TestCase::failOnWarning(message) + \since 6.3 + + Appends a test failure to the test log for each warning that matches + \a message. The test function will continue execution when a failure + is added. + + \a message can be either a string, or a regular expression providing a + pattern of messages. In the latter case, for each warning encountered, + the first pattern that matches will cause a failure, and the remaining + patterns will be ignored. + + All patterns are cleared at the end of each test function. + + For example, the following snippet will fail a test if a warning with + the text "Something bad happened" is produced: + \qml + failOnWarning("Something bad happened") + \endqml + + The following snippet will fail a test if any warning matching the + given pattern is encountered: + \qml + failOnWarning(/[0-9]+ bad things happened/) + \endqml + + To fail every test that triggers a given warning, pass a suitable regular + expression to this function in \l init(): + + \qml + function init() { + failOnWarning(/.?/) + } + \endqml + + \note Despite being a JavaScript RegExp object, it will not be + interpreted as such; instead, the pattern will be passed to \l + QRegularExpression. + + \note ignoreMessage() takes precedence over this function, so any + warnings that match a pattern given to both \c ignoreMessage() and \c + failOnWarning() will be ignored. + + \sa QTest::failOnWarning(), warn() + */ + function failOnWarning(msg) { + if (msg === undefined) + msg = "" + qtest_results.failOnWarning(msg) + } + + /*! + \qmlmethod TestCase::wait(ms) + + Waits for \a ms milliseconds while processing Qt events. + + \note This methods uses a precise timer to do the actual waiting. The + event you are waiting for may not. In particular, any animations as + well as the \l{Timer} QML type can use either precise or coarse + timers, depending on various factors. For a coarse timer you have + to expect a drift of around 5% in relation to the precise timer used + by TestCase::wait(). Qt cannot give hard guarantees on the drift, + though, because the operating system usually doesn't offer hard + guarantees on timers. + + \sa sleep(), waitForRendering(), Qt::TimerType + */ + function wait(ms) { + qtest_results.wait(ms) + } + + /*! + \qmlmethod TestCase::waitForRendering(item, timeout = 5000) + + Waits for \a timeout milliseconds or until the \a item is rendered by the renderer. + Returns true if \c item is rendered in \a timeout milliseconds, otherwise returns false. + The default \a timeout value is 5000. + + \sa sleep(), wait() + */ + function waitForRendering(item, timeout) { + if (timeout === undefined) + timeout = 5000 + if (!qtest_verifyItem(item, "waitForRendering")) + return + return qtest_results.waitForRendering(item, timeout) + } + + /*! + \qmlmethod TestCase::sleep(ms) + + Sleeps for \a ms milliseconds without processing Qt events. + + \sa wait(), waitForRendering() + */ + function sleep(ms) { + qtest_results.sleep(ms) + } + + /*! + \qmlmethod TestCase::keyPress(key, modifiers = Qt.NoModifier, delay = -1) + + Simulates pressing a \a key with optional \a modifiers on the currently + focused item. If \a delay is larger than 0, the test will wait for + \a delay milliseconds. + + The event will be sent to the TestCase window or, in case of multiple windows, + to the current active window. See \l QGuiApplication::focusWindow() for more details. + + \b{Note:} At some point you should release the key using keyRelease(). + + \sa keyRelease(), keyClick() + */ + function keyPress(key, modifiers, delay) { + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (typeof(key) === "string" && key.length === 1) { + if (!qtest_events.keyPressChar(key, modifiers, delay)) + qtest_fail("window not shown", 2) + } else { + if (!qtest_events.keyPress(key, modifiers, delay)) + qtest_fail("window not shown", 2) + } + } + + /*! + \qmlmethod TestCase::keyRelease(key, modifiers = Qt.NoModifier, delay = -1) + + Simulates releasing a \a key with optional \a modifiers on the currently + focused item. If \a delay is larger than 0, the test will wait for + \a delay milliseconds. + + The event will be sent to the TestCase window or, in case of multiple windows, + to the current active window. See \l QGuiApplication::focusWindow() for more details. + + \sa keyPress(), keyClick() + */ + function keyRelease(key, modifiers, delay) { + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (typeof(key) === "string" && key.length === 1) { + if (!qtest_events.keyReleaseChar(key, modifiers, delay)) + qtest_fail("window not shown", 2) + } else { + if (!qtest_events.keyRelease(key, modifiers, delay)) + qtest_fail("window not shown", 2) + } + } + + /*! + \qmlmethod TestCase::keyClick(key, modifiers = Qt.NoModifier, delay = -1) + + Simulates clicking of \a key with optional \a modifiers on the currently + focused item. If \a delay is larger than 0, the test will wait for + \a delay milliseconds. + + The event will be sent to the TestCase window or, in case of multiple windows, + to the current active window. See \l QGuiApplication::focusWindow() for more details. + + \sa keyPress(), keyRelease() + */ + function keyClick(key, modifiers, delay) { + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (typeof(key) === "string" && key.length === 1) { + if (!qtest_events.keyClickChar(key, modifiers, delay)) + qtest_fail("window not shown", 2) + } else { + if (!qtest_events.keyClick(key, modifiers, delay)) + qtest_fail("window not shown", 2) + } + } + + /*! + \since 5.10 + \qmlmethod TestCase::keySequence(keySequence) + + Simulates typing of \a keySequence. The key sequence can be set + to one of the \l{QKeySequence::StandardKey}{standard keyboard shortcuts}, or + it can be described with a string containing a sequence of up to four key + presses. + + Each event shall be sent to the TestCase window or, in case of multiple windows, + to the current active window. See \l QGuiApplication::focusWindow() for more details. + + \sa keyPress(), keyRelease(), {GNU Emacs Style Key Sequences}, + {QtQuick::Shortcut::sequence}{Shortcut.sequence} + */ + function keySequence(keySequence) { + if (!qtest_events.keySequence(keySequence)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TestCase::mousePress(item, x = item.width / 2, y = item.height / 2, button = Qt.LeftButton, modifiers = Qt.NoModifier, delay = -1) + + Simulates pressing a mouse \a button with optional \a modifiers + on an \a item. The position is defined by \a x and \a y. + If \a x or \a y are not defined the position will be the center of \a item. + If \a delay is specified, the test will wait for the specified amount of + milliseconds before the press. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + \sa mouseRelease(), mouseClick(), mouseDoubleClickSequence(), mouseMove(), mouseDrag(), mouseWheel() + */ + function mousePress(item, x, y, button, modifiers, delay) { + if (!qtest_verifyItem(item, "mousePress")) + return + + if (button === undefined) + button = Qt.LeftButton + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (x === undefined) + x = item.width / 2 + if (y === undefined) + y = item.height / 2 + if (!qtest_events.mousePress(item, x, y, button, modifiers, delay)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TestCase::mouseRelease(item, x = item.width / 2, y = item.height / 2, button = Qt.LeftButton, modifiers = Qt.NoModifier, delay = -1) + + Simulates releasing a mouse \a button with optional \a modifiers + on an \a item. The position of the release is defined by \a x and \a y. + If \a x or \a y are not defined the position will be the center of \a item. + If \a delay is specified, the test will wait for the specified amount of + milliseconds before releasing the button. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + \sa mousePress(), mouseClick(), mouseDoubleClickSequence(), mouseMove(), mouseDrag(), mouseWheel() + */ + function mouseRelease(item, x, y, button, modifiers, delay) { + if (!qtest_verifyItem(item, "mouseRelease")) + return + + if (button === undefined) + button = Qt.LeftButton + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (x === undefined) + x = item.width / 2 + if (y === undefined) + y = item.height / 2 + if (!qtest_events.mouseRelease(item, x, y, button, modifiers, delay)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TestCase::mouseDrag(item, x, y, dx, dy, button = Qt.LeftButton, modifiers = Qt.NoModifier, delay = -1) + + Simulates dragging the mouse on an \a item with \a button pressed and optional \a modifiers + The initial drag position is defined by \a x and \a y, + and drag distance is defined by \a dx and \a dy. If \a delay is specified, + the test will wait for the specified amount of milliseconds before releasing the button. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + \sa mousePress(), mouseClick(), mouseDoubleClickSequence(), mouseMove(), mouseRelease(), mouseWheel() + */ + function mouseDrag(item, x, y, dx, dy, button, modifiers, delay) { + if (!qtest_verifyItem(item, "mouseDrag")) + return + + if (item.x === undefined || item.y === undefined) + return + if (button === undefined) + button = Qt.LeftButton + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + let moveDelay = Math.max(1, delay === -1 ? qtest_events.defaultMouseDelay : delay) + + // Divide dx and dy to have intermediate mouseMove while dragging + // Fractions of dx/dy need be superior to the dragThreshold + // to make the drag works though + let intermediateDx = Math.round(dx/3) + if (Math.abs(intermediateDx) < (util.dragThreshold + 1)) + intermediateDx = 0 + let intermediateDy = Math.round(dy/3) + if (Math.abs(intermediateDy) < (util.dragThreshold + 1)) + intermediateDy = 0 + + mousePress(item, x, y, button, modifiers, delay) + + // Trigger dragging by dragging past the drag threshold, but making sure to only drag + // along a certain axis if a distance greater than zero was given for that axis. + let dragTriggerXDistance = dx > 0 ? (util.dragThreshold + 1) : 0 + let dragTriggerYDistance = dy > 0 ? (util.dragThreshold + 1) : 0 + mouseMove(item, x + dragTriggerXDistance, y + dragTriggerYDistance, moveDelay, button, modifiers) + if (intermediateDx !== 0 || intermediateDy !== 0) { + mouseMove(item, x + intermediateDx, y + intermediateDy, moveDelay, button, modifiers) + mouseMove(item, x + 2*intermediateDx, y + 2*intermediateDy, moveDelay, button, modifiers) + } + mouseMove(item, x + dx, y + dy, moveDelay, button, modifiers) + mouseRelease(item, x + dx, y + dy, button, modifiers, delay) + } + + /*! + \qmlmethod TestCase::mouseClick(item, x = item.width / 2, y = item.height / 2, button = Qt.LeftButton, modifiers = Qt.NoModifier, delay = -1) + + Simulates clicking a mouse \a button with optional \a modifiers + on an \a item. The position of the click is defined by \a x and \a y. + If \a x and \a y are not defined the position will be the center of \a item. + If \a delay is specified, the test will wait for the specified amount of + milliseconds before pressing and before releasing the button. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + \sa mousePress(), mouseRelease(), mouseDoubleClickSequence(), mouseMove(), mouseDrag(), mouseWheel() + */ + function mouseClick(item, x, y, button, modifiers, delay) { + if (!qtest_verifyItem(item, "mouseClick")) + return + + if (button === undefined) + button = Qt.LeftButton + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (x === undefined) + x = item.width / 2 + if (y === undefined) + y = item.height / 2 + if (!qtest_events.mouseClick(item, x, y, button, modifiers, delay)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TestCase::mouseDoubleClickSequence(item, x = item.width / 2, y = item.height / 2, button = Qt.LeftButton, modifiers = Qt.NoModifier, delay = -1) + + Simulates the full sequence of events generated by double-clicking a mouse + \a button with optional \a modifiers on an \a item. + + This method reproduces the sequence of mouse events generated when a user makes + a double click: Press-Release-Press-DoubleClick-Release. + + The position of the click is defined by \a x and \a y. + If \a x and \a y are not defined the position will be the center of \a item. + If \a delay is specified, the test will wait for the specified amount of + milliseconds before pressing and before releasing the button. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + This QML method was introduced in Qt 5.5. + + \sa mousePress(), mouseRelease(), mouseClick(), mouseMove(), mouseDrag(), mouseWheel() + */ + function mouseDoubleClickSequence(item, x, y, button, modifiers, delay) { + if (!qtest_verifyItem(item, "mouseDoubleClickSequence")) + return + + if (button === undefined) + button = Qt.LeftButton + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (delay === undefined) + delay = -1 + if (x === undefined) + x = item.width / 2 + if (y === undefined) + y = item.height / 2 + if (!qtest_events.mouseDoubleClickSequence(item, x, y, button, modifiers, delay)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TestCase::mouseMove(item, x = item.width / 2, y = item.height / 2, delay = -1, buttons = Qt.NoButton) + + Moves the mouse pointer to the position given by \a x and \a y within + \a item, while holding \a buttons if given. Since Qt 6.0, if \a x and + \a y are not defined, the position will be the center of \a item. + + If a \a delay (in milliseconds) is given, the test will wait before + moving the mouse pointer. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + \sa mousePress(), mouseRelease(), mouseClick(), mouseDoubleClickSequence(), mouseDrag(), mouseWheel() + */ + function mouseMove(item, x, y, delay, buttons, modifiers) { + if (!qtest_verifyItem(item, "mouseMove")) + return + + if (delay === undefined) + delay = -1 + if (buttons === undefined) + buttons = Qt.NoButton + if (modifiers === undefined) + modifiers = Qt.NoModifiers + if (x === undefined) + x = item.width / 2 + if (y === undefined) + y = item.height / 2 + if (!qtest_events.mouseMove(item, x, y, delay, buttons, modifiers)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TestCase::mouseWheel(item, x, y, xDelta, yDelta, button = Qt.LeftButton, modifiers = Qt.NoModifier, delay = -1) + + Simulates rotating the mouse wheel on an \a item with \a button pressed and optional \a modifiers. + The position of the wheel event is defined by \a x and \a y. + If \a delay is specified, the test will wait for the specified amount of milliseconds before releasing the button. + + The position given by \a x and \a y is transformed from the co-ordinate + system of \a item into window co-ordinates and then delivered. + If \a item is obscured by another item, or a child of \a item occupies + that position, then the event will be delivered to the other item instead. + + The \a xDelta and \a yDelta contain the wheel rotation distance in eighths of a degree. see \l QWheelEvent::angleDelta() for more details. + + \sa mousePress(), mouseClick(), mouseDoubleClickSequence(), mouseMove(), mouseRelease(), mouseDrag(), QWheelEvent::angleDelta() + */ + function mouseWheel(item, x, y, xDelta, yDelta, buttons, modifiers, delay) { + if (!qtest_verifyItem(item, "mouseWheel")) + return + + if (delay === undefined) + delay = -1 + if (buttons === undefined) + buttons = Qt.NoButton + if (modifiers === undefined) + modifiers = Qt.NoModifier + if (xDelta === undefined) + xDelta = 0 + if (yDelta === undefined) + yDelta = 0 + if (!qtest_events.mouseWheel(item, x, y, buttons, modifiers, xDelta, yDelta, delay)) + qtest_fail("window not shown", 2) + } + + /*! + \qmlmethod TouchEventSequence TestCase::touchEvent(object item) + + \since 5.9 + + Begins a sequence of touch events through a simulated touchscreen (QPointingDevice). + Events are delivered to the window containing \a item. + + The returned object is used to enumerate events to be delivered through a single + QTouchEvent. Touches are delivered to the window containing the TestCase unless + otherwise specified. + + \code + Rectangle { + width: 640; height: 480 + + MultiPointTouchArea { + id: area + anchors.fill: parent + + property bool touched: false + + onPressed: touched = true + } + + TestCase { + name: "ItemTests" + when: windowShown + id: test1 + + function test_touch() { + let touch = touchEvent(area); + touch.press(0, area, 10, 10); + touch.commit(); + verify(area.touched); + } + } + } + \endcode + + \sa TouchEventSequence::press(), TouchEventSequence::move(), TouchEventSequence::release(), TouchEventSequence::stationary(), TouchEventSequence::commit(), QInputDevice::DeviceType + */ + + function touchEvent(item) { + if (!qtest_verifyItem(item, "touchEvent")) + return + + return { + _defaultItem: item, + _sequence: qtest_events.touchEvent(item), + + press: function (id, target, x, y) { + if (!target) + target = this._defaultItem; + if (id === undefined) + qtest_fail("No id given to TouchEventSequence::press", 1); + if (x === undefined) + x = target.width / 2; + if (y === undefined) + y = target.height / 2; + this._sequence.press(id, target, x, y); + return this; + }, + + move: function (id, target, x, y) { + if (!target) + target = this._defaultItem; + if (id === undefined) + qtest_fail("No id given to TouchEventSequence::move", 1); + if (x === undefined) + x = target.width / 2; + if (y === undefined) + y = target.height / 2; + this._sequence.move(id, target, x, y); + return this; + }, + + stationary: function (id) { + if (id === undefined) + qtest_fail("No id given to TouchEventSequence::stationary", 1); + this._sequence.stationary(id); + return this; + }, + + release: function (id, target, x, y) { + if (!target) + target = this._defaultItem; + if (id === undefined) + qtest_fail("No id given to TouchEventSequence::release", 1); + if (x === undefined) + x = target.width / 2; + if (y === undefined) + y = target.height / 2; + this._sequence.release(id, target, x, y); + return this; + }, + + commit: function () { + this._sequence.commit(); + return this; + } + }; + } + + // Functions that can be overridden in subclasses for init/cleanup duties. + /*! + \qmlmethod TestCase::initTestCase() + + This function is called before any other test functions in the + \l TestCase type. The default implementation does nothing. + The application can provide its own implementation to perform + test case initialization. + + \sa cleanupTestCase(), init() + */ + function initTestCase() {} + + /*! + \qmlmethod TestCase::cleanupTestCase() + + This function is called after all other test functions in the + \l TestCase type have completed. The default implementation + does nothing. The application can provide its own implementation + to perform test case cleanup. + + \sa initTestCase(), cleanup() + */ + function cleanupTestCase() {} + + /*! + \qmlmethod TestCase::init() + + This function is called before each test function that is + executed in the \l TestCase type. The default implementation + does nothing. The application can provide its own implementation + to perform initialization before each test function. + + \sa cleanup(), initTestCase() + */ + function init() {} + + /*! + \qmlmethod TestCase::cleanup() + + This function is called after each test function that is + executed in the \l TestCase type. The default implementation + does nothing. The application can provide its own implementation + to perform cleanup after each test function. + + \sa init(), cleanupTestCase() + */ + function cleanup() {} + + /*! \internal */ + function qtest_verifyItem(item, method) { + try { + if (!(item instanceof Item) && + !(item instanceof Window)) { + // it's a QObject, but not a type + qtest_fail("TypeError: %1 requires an Item or Window type".arg(method), 2); + return false; + } + } catch (e) { // it's not a QObject + qtest_fail("TypeError: %1 requires an Item or Window type".arg(method), 3); + return false; + } + + return true; + } + + /*! \internal */ + function qtest_runInternal(prop, arg) { + try { + qtest_testCaseResult = testCase[prop](arg) + } catch (e) { + qtest_testCaseResult = [] + if (e.message.indexOf("QtQuickTest::") !== 0) { + // Test threw an unrecognized exception - fail. + qtest_results.fail("Uncaught exception: " + e.message, + e.fileName, e.lineNumber) + } + } + return !qtest_results.failed + } + + /*! \internal */ + function qtest_runFunction(prop, arg) { + qtest_runInternal("init") + if (!qtest_results.skipped) { + qtest_runInternal(prop, arg) + qtest_results.finishTestData() + qtest_runInternal("cleanup") + qtest_destroyTemporaryObjects() + + // wait(0) will call processEvents() so objects marked for deletion + // in the test function will be deleted. + wait(0) + + qtest_results.finishTestDataCleanup() + } + } + + /*! \internal */ + function qtest_runBenchmarkFunction(prop, arg) { + qtest_results.startMeasurement() + do { + qtest_results.beginDataRun() + do { + // Run the initialization function. + qtest_runInternal("init") + if (qtest_results.skipped) + break + + // Execute the benchmark function. + if (prop.indexOf("benchmark_once_") !== 0) + qtest_results.startBenchmark(TestResult.RepeatUntilValidMeasurement, qtest_results.dataTag) + else + qtest_results.startBenchmark(TestResult.RunOnce, qtest_results.dataTag) + while (!qtest_results.isBenchmarkDone()) { + let success = qtest_runInternal(prop, arg) + qtest_results.finishTestData() + if (!success) + break + qtest_results.nextBenchmark() + } + qtest_results.stopBenchmark() + + // Run the cleanup function. + qtest_runInternal("cleanup") + qtest_results.finishTestDataCleanup() + // wait(0) will call processEvents() so objects marked for deletion + // in the test function will be deleted. + wait(0) + } while (!qtest_results.measurementAccepted()) + qtest_results.endDataRun() + } while (qtest_results.needsMoreMeasurements()) + } + + /*! \internal */ + function qtest_run() { + if (!when || completed || running || !qtest_componentCompleted) + return; + + if (!TestLogger.log_can_start_test(qtest_testId)) { + console.error("Interleaved test execution detected. This shouldn't happen") + return; + } + + if (TestLogger.log_start_test(qtest_testId)) { + qtest_results.reset() + qtest_results.testCaseName = name + qtest_results.startLogging() + } else { + qtest_results.testCaseName = name + } + running = true + + // Check the run list to see if this class is mentioned. + let checkNames = false + let testsToRun = {} // explicitly provided function names to run and their tags for data-driven tests + + if (qtest_results.functionsToRun.length > 0) { + checkNames = true + let found = false + + if (name.length > 0) { + for (let index in qtest_results.functionsToRun) { + let caseFuncName = qtest_results.functionsToRun[index] + if (caseFuncName.indexOf(name + "::") !== 0) + continue + + found = true + let funcName = caseFuncName.substring(name.length + 2) + + if (!(funcName in testsToRun)) + testsToRun[funcName] = [] + + let tagName = qtest_results.tagsToRun[index] + if (tagName.length > 0) // empty tags mean run all rows + testsToRun[funcName].push(tagName) + } + } + if (!found) { + completed = true + if (!TestLogger.log_complete_test(qtest_testId)) { + qtest_results.stopLogging() + Qt.quit() + } + qtest_results.testCaseName = "" + return + } + } + + // Run the initTestCase function. + qtest_results.functionName = "initTestCase" + let runTests = true + if (!qtest_runInternal("initTestCase")) + runTests = false + qtest_results.finishTestData() + qtest_results.finishTestDataCleanup() + qtest_results.finishTestFunction() + + // Run the test methods. + let testList = [] + if (runTests) { + for (let prop in testCase) { + if (prop.indexOf("test_") !== 0 && prop.indexOf("benchmark_") !== 0) + continue + let tail = prop.lastIndexOf("_data"); + if (tail !== -1 && tail === (prop.length - 5)) + continue + testList.push(prop) + } + testList.sort() + } + + for (let index in testList) { + let prop = testList[index] + + if (checkNames && !(prop in testsToRun)) + continue + + let datafunc = prop + "_data" + let isBenchmark = (prop.indexOf("benchmark_") === 0) + qtest_results.functionName = prop + + if (!(datafunc in testCase)) + datafunc = "init_data"; + + if (datafunc in testCase) { + if (qtest_runInternal(datafunc)) { + let table = qtest_testCaseResult + let haveData = false + + let checkTags = (checkNames && testsToRun[prop].length > 0) + + qtest_results.initTestTable() + for (let index in table) { + haveData = true + let row = table[index] + if (!row.tag) + row.tag = "row " + index // Must have something + if (checkTags) { + let tags = testsToRun[prop] + let tagIdx = tags.indexOf(row.tag) + if (tagIdx < 0) + continue + tags.splice(tagIdx, 1) + } + qtest_results.dataTag = row.tag + if (isBenchmark) + qtest_runBenchmarkFunction(prop, row) + else + qtest_runFunction(prop, row) + qtest_results.dataTag = "" + qtest_results.skipped = false + } + if (!haveData) { + if (datafunc === "init_data") + qtest_runFunction(prop, null, isBenchmark) + else + qtest_results.warn("no data supplied for " + prop + "() by " + datafunc + "()" + , util.callerFile(), util.callerLine()); + } + qtest_results.clearTestTable() + } + } else if (isBenchmark) { + qtest_runBenchmarkFunction(prop, null, isBenchmark) + } else { + qtest_runFunction(prop, null, isBenchmark) + } + qtest_results.finishTestFunction() + qtest_results.skipped = false + + if (checkNames && testsToRun[prop].length <= 0) + delete testsToRun[prop] + } + + // Run the cleanupTestCase function. + qtest_results.skipped = false + qtest_results.functionName = "cleanupTestCase" + qtest_runInternal("cleanupTestCase") + + // Complain about missing functions that we were supposed to run. + if (checkNames) { + let missingTests = [] + for (let func in testsToRun) { + let caseFuncName = name + '::' + func + let tags = testsToRun[func] + if (tags.length <= 0) + missingTests.push(caseFuncName) + else + for (let i in tags) + missingTests.push(caseFuncName + ':' + tags[i]) + } + missingTests.sort() + if (missingTests.length > 0) + qtest_results.fail("Could not find test functions: " + missingTests, "", 0) + } + + // Clean up and exit. + running = false + completed = true + qtest_results.finishTestData() + qtest_results.finishTestDataCleanup() + qtest_results.finishTestFunction() + qtest_results.functionName = "" + + // Stop if there are no more tests to be run. + if (!TestLogger.log_complete_test(qtest_testId)) { + qtest_results.stopLogging() + Qt.quit() + } + qtest_results.testCaseName = "" + } + + onWhenChanged: { + if (when !== qtest_prevWhen) { + qtest_prevWhen = when + if (when) + TestSchedule.testCases.push(testCase) + } + } + + onOptionalChanged: { + if (!completed) { + if (optional) + TestLogger.log_optional_test(qtest_testId) + else + TestLogger.log_mandatory_test(qtest_testId) + } + } + + Component.onCompleted: { + QTestRootObject.hasTestCase = true; + qtest_componentCompleted = true; + qtest_testId = TestLogger.log_register_test(name) + if (optional) + TestLogger.log_optional_test(qtest_testId) + qtest_prevWhen = when + if (when) + TestSchedule.testCases.push(testCase) + } +} |