diff --git a/.clang-format b/.clang-format index 8e5a51c..1a17df3 100644 --- a/.clang-format +++ b/.clang-format @@ -11,6 +11,7 @@ AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false AllowShortFunctionsOnASingleLine: InlineOnly AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e5adf27 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: faaxm +custom: "https://www.paypal.me/faaxm" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a301c7d..54df889 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,9 +11,10 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-20.04, ubuntu-18.04, windows-latest, macos-latest, macos-10.15] + os: [ubuntu-latest, ubuntu-20.04, windows-latest, macos-latest, macos-10.15] build_type: ['Release', 'Debug'] shared_libs: ['ON', 'OFF'] + qt_version: [[5, 12, 12], [5, 15, 2], [6, 2, 3]] include: - os: ubuntu-latest triplet: 'x64-linux' @@ -24,9 +25,6 @@ jobs: - os: ubuntu-20.04 triplet: 'x64-linux' cmake_flags: '-DCMAKE_CXX_COMPILER=g++' - - os: ubuntu-18.04 - triplet: 'x64-linux' - cmake_flags: '-DCMAKE_CXX_COMPILER=g++' - os: windows-latest triplet: 'x64-windows' cmake_flags: '"-DAnyRPC_ROOT=C:/Program Files (x86)/AnyRPC" "-DGTest_ROOT=C:/Program Files (x86)/googletest-distribution"' @@ -44,8 +42,19 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 + - name: Cache Qt + id: cache-qt + uses: actions/cache@v1 # not v2! + with: + path: ../Qt + key: ${{ runner.os }}-${{ join(matrix.qt_version, '.') }}-QtCache + - name: Install Qt uses: jurplel/install-qt-action@v2 + with: + aqtversion: '>=1.2.1' + version: ${{ join(matrix.qt_version, '.') }} + cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Install dependencies env: @@ -56,7 +65,7 @@ jobs: - name: "Configure" run: | mkdir build - cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DSPIX_BUILD_TESTS=ON -DSPIX_BUILD_EXAMPLES=ON ${{ matrix.cmake_flags}} -DBUILD_SHARED_LIBS=${{ matrix.shared_libs }} . + cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DSPIX_BUILD_TESTS=ON -DSPIX_BUILD_EXAMPLES=ON ${{ matrix.cmake_flags}} -DBUILD_SHARED_LIBS=${{ matrix.shared_libs }} -DSPIX_QT_MAJOR=${{ matrix.qt_version[0] }} . - name: "Print cmake compile commands" if: ${{ !contains(matrix.os, 'windows') }} @@ -68,6 +77,19 @@ jobs: - name: "Run Tests" run: cd build && ctest -VV -C ${{ matrix.build_type }} && cd .. + - name: "Test GTest Examples (*nix)" + # Qt6 on GitHub Actions MacOS is currently bugged: https://bugreports.qt.io/browse/QTIFW-1592 + if: ${{ !contains(matrix.os, 'windows') && !(contains(matrix.os, 'mac') && matrix.qt_version[0] == 6) }} + run: | + build/examples/GTest/SpixGTestExample -platform minimal + build/examples/RepeaterLoader/SpixRepeaterLoaderExampleGTest -platform minimal + + - name: "Test GTest Examples (win)" + if: ${{ contains(matrix.os, 'windows') }} + run: | + .\build\examples\GTest\${{ matrix.build_type }}\SpixGTestExample.exe -platform minimal + .\build\examples\RepeaterLoader\${{ matrix.build_type }}\SpixRepeaterLoaderExampleGTest.exe -platform minimal + - name: "Install Spix (*nix)" if: ${{ !contains(matrix.os, 'windows') }} run: sudo cmake --install build --config ${{ matrix.build_type }} @@ -80,7 +102,7 @@ jobs: run: | cd examples/BasicStandalone mkdir build - cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.cmake_flags}} . + cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DSPIX_QT_MAJOR=${{ matrix.qt_version[0] }} ${{ matrix.cmake_flags}} . cmake --build build --config ${{ matrix.build_type }} diff --git a/CMakeLists.txt b/CMakeLists.txt index ff86414..7aea3f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,11 @@ -cmake_minimum_required(VERSION 3.12) +cmake_minimum_required(VERSION 3.18) project(Spix LANGUAGES CXX) option(SPIX_BUILD_EXAMPLES "Build Spix examples." ON) option(SPIX_BUILD_TESTS "Build Spix unit tests." OFF) +set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") -set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake/modules") set(CMAKE_CXX_STANDARD 14) # Hide symbols unless explicitly flagged with SPIX_EXPORT diff --git a/README.md b/README.md index 40e31eb..c7f839e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ [![Build and Test](https://github.com/faaxm/spix/actions/workflows/build.yml/badge.svg)](https://github.com/faaxm/spix/actions/workflows/build.yml) -[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://www.paypal.me/faaxm) + +

+ Spix Logo +

# Spix Spix is a minimally invasive UI testing library that enables your @@ -41,27 +44,9 @@ and make sure that it behaves as you expect. However, you can also use Spix as an easy way to remote control existing Qt/QML applications or to automatically generate and update screenshots for your documentation. -## Two modes of operation -Spix can be used in two ways, which are different in how events are generated and sent -to your application: - -### Generate Qt events directly -You can use Spix to directly create Qt events, either from C++ as a unit test, or from -an external script via the network through RPC. Since the Qt events are generated directly inside the -app, and do not come from the system, the mouse coursor will not actually move and interaction -with other applications is limited. On the plus side, this mechanism is independent from -the system your app is running on and can easily be used to control software on an embedded -device via the network (RPC). - -### Generate system events externally -In this case, Spix is not generating the events itself. Instead, you use a script to query -Spix for the screen coordinates of qt objects and then generate events on the system level -through other tools. One option is to use python together with PyAutoGUI for this, as is -done in the [RemoteCtrl](examples/RemoteCtrl) example. - # Requirements -* Qt -* AnyRPC +* Qt (both 5 and 6 supported) +* [AnyRPC](https://github.com/sgieseking/anyrpc) # Current Features * Send mouse events (click, move, drag/drop) @@ -73,21 +58,131 @@ done in the [RemoteCtrl](examples/RemoteCtrl) example. * Quit the app * Remote control, also of embedded devices / iOS -# Building Spix +# Setting Up Spix + +## Installing AnyRPC + +Spix requires [AnyRPC](https://github.com/sgieseking/anyrpc) be installed on the local machine before building. For *nix machines, AnyRPC can be built/installed using CMake: +```sh +# in a temporary directory +git clone https://github.com/sgieseking/anyrpc.git +cd anyrpc +mkdir build +cd build +cmake -DBUILD_EXAMPLES=OFF -DBUILD_WITH_LOG4CPLUS=OFF -DBUILD_PROTOCOL_MESSAGEPACK=OFF .. +cmake --build . +sudo cmake --install . +``` + +For non-*nix machines, checkout the [CI install script](ci/install-deps.sh). + +## Installing Spix + Spix uses cmake and can be build with the standard cmake commands once cloned: ```sh git clone https://github.com/faaxm/spix cd spix mkdir build && cd build -cmake .. +cmake -DSPIX_QT_MAJOR=6 .. cmake --build . +sudo cmake --install . ``` -You can also have a look at the build scripts in `ci/`, which are run on the -build server to build and test Spix. +> Change SPIX_QT_MAJOR to 5 to build against Qt5 instead of Qt6. -If you installed the dependencies (like AnyRPC) in a non-standard directory -you can point cmake to it by setting `CMAKE_PREFIX_PATH`, so +If you installed the dependencies (like AnyRPC) in a non-standard directory you can point cmake to it by setting `CMAKE_PREFIX_PATH`, so instead of `cmake ..` you run: ```sh cmake -DCMAKE_PREFIX_PATH=/path/to/libs .. ``` + +## Including Spix in your Qt project + +If using qmake, add the following to your Qt `.pro` file: +``` +QT += quick +INCLUDEPATH += /usr/local/include +LIBS += -lSpix -lanyrpc +``` +If using CMake, add the following to your `CMakeLists.txt`: +``` +find_package(Spix REQUIRED) +``` + +Update your `main(...)` to start the Spix RPC server: +```C++ +#include +#include + +int main(...) { + ... + spix::AnyRpcServer server; + auto bot = new spix::QtQmlBot(); + bot->runTestServer(server); + ... +} +``` +Finally, if you're using a `QQuickView` as your root window, you'll need to give it an object name in your `main` (otherwise the root window object name will be defined in your QML): +```C++ +int main(...) { + QQuickView view; + view.setObjectName("root") + ... +} +``` + +# Using Spix + +The easiest method of interacting with Spix is using the [XMLRPC client built into python](https://docs.python.org/3/library/xmlrpc.client.html#module-xmlrpc.client): +```python +import xmlrpc.client + +s = xmlrpc.client.ServerProxy('http://localhost:9000') # default port is 9000 +s.method(, ) +# for example: +s.mouseClick("root/Button_2") +resultText = s.getStringProperty("root/results", "text") +``` + +You can also use the XMLRPC client to list the available methods. The complete list of methods are also available in the [source](lib/src/AnyRpcServer.cpp). +```python +print(s.system.listMethods()) +# ['command', 'enterKey', 'existsAndVisible', 'getBoundingBox', 'getErrors', 'getStringProperty', 'inputText', 'mouseBeginDrag', 'mouseClick', 'mouseDropUrls', 'mouseEndDrag', 'quit', 'setStringProperty', 'system.listMethods', 'system.methodHelp', 'takeScreenshot', 'wait'] +print(s.system.methodHelp('mouseClick')) +# Click on the object at the given path +``` + +Spix uses a slash-separated path format to select Qt objects. Selectors match against `objectName` or `id` if no object name is defined. +``` +/(/...) +``` +Spix matches children recursivley, allowing as much flexibility as needed: +``` +# matches any `button` that is a descendant of `root` (even subchildren) +'root/button' +# matches any `button` that is a descendant of `numberpad` which is in turn a descendant of `root`. +'root/numberpad/button' +# and so on +``` + +More specifically, Spix's matching processes works as follows: +* `` matches a top-level [`QQuickWindow`](https://doc-snapshots.qt.io/qt6-dev/qquickwindow.html) whose `objectName` (or `id` if `objectName` is empty) matches the specified string. Top-level windows are enumerated by [`QGuiApplication::topLevelWindows`](https://doc.qt.io/qt-6/qguiapplication.html#topLevelWindows). +* `` matches the first child object whose `objectName` (or `id` if `objectName` is empty) matches the specified string using a recursive search of all children and subchildren of the root. This process repeats for every subsequent child path entry. + + +## Two modes of operation +In general, Spix can be used in two ways, which are different in how events are generated and sent +to your application: + +### Generate Qt events directly +You can use Spix to directly create Qt events, either from C++ as a unit test, or from +an external script via the network through RPC. Since the Qt events are generated directly inside the +app, and do not come from the system, the mouse coursor will not actually move and interaction +with other applications is limited. On the plus side, this mechanism is independent from +the system your app is running on and can easily be used to control software on an embedded +device via the network (RPC). + +### Generate system events externally +In this case, Spix is not generating the events itself. Instead, you use a script to query +Spix for the screen coordinates of qt objects and then generate events on the system level +through other tools. One option is to use python together with PyAutoGUI for this, as is +done in the [RemoteCtrl](examples/RemoteCtrl) example. diff --git a/ci/install-deps.sh b/ci/install-deps.sh index a322562..568f5a1 100644 --- a/ci/install-deps.sh +++ b/ci/install-deps.sh @@ -6,7 +6,7 @@ mkdir deps && cd deps git clone --recursive https://github.com/sgieseking/anyrpc.git cd anyrpc && mkdir build && cd build if [[ $RUNNER_OS == "Windows" ]]; then - cmake -G"Visual Studio 16 2019" -DCMAKE_BUILD_TYPE=$CI_BUILD_TYPE -DBUILD_EXAMPLES=OFF -DBUILD_WITH_LOG4CPLUS=OFF -DANYRPC_LIB_BUILD_SHARED=${SHARED_LIBS} .. + cmake -G"Visual Studio 17 2022" -DCMAKE_BUILD_TYPE=$CI_BUILD_TYPE -DBUILD_EXAMPLES=OFF -DBUILD_WITH_LOG4CPLUS=OFF -DANYRPC_LIB_BUILD_SHARED=${SHARED_LIBS} .. cmake --build . --target install --config $CI_BUILD_TYPE else cmake -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=$CI_BUILD_TYPE -DBUILD_EXAMPLES=OFF -DBUILD_WITH_LOG4CPLUS=OFF -DANYRPC_LIB_BUILD_SHARED=${SHARED_LIBS} .. @@ -18,7 +18,7 @@ cd ../.. git clone --recursive https://github.com/google/googletest.git cd googletest && mkdir build && cd build if [[ $RUNNER_OS == "Windows" ]]; then - cmake -G"Visual Studio 16 2019" -Dgtest_force_shared_crt=ON -DCMAKE_BUILD_TYPE=$CI_BUILD_TYPE .. + cmake -G"Visual Studio 17 2022" -Dgtest_force_shared_crt=ON -DCMAKE_BUILD_TYPE=$CI_BUILD_TYPE .. cmake --build . --target install --config $CI_BUILD_TYPE else cmake -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=$CI_BUILD_TYPE .. diff --git a/examples/Basic/CMakeLists.txt b/examples/Basic/CMakeLists.txt index a308028..23885d3 100644 --- a/examples/Basic/CMakeLists.txt +++ b/examples/Basic/CMakeLists.txt @@ -4,8 +4,8 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) add_executable(SpixBasicExample "main.cpp" "qml.qrc") target_compile_definitions(SpixBasicExample PRIVATE $<$,$>:QT_QML_DEBUG>) -target_link_libraries(SpixBasicExample PRIVATE Qt5::Core Qt5::Quick Spix) +target_link_libraries(SpixBasicExample PRIVATE Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick Spix) diff --git a/examples/Basic/main.cpp b/examples/Basic/main.cpp index 5622bae..fd45b83 100644 --- a/examples/Basic/main.cpp +++ b/examples/Basic/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -26,6 +27,8 @@ class MyTests : public spix::TestServer { wait(std::chrono::milliseconds(500)); mouseClick(spix::ItemPath("mainWindow/Button_1")); wait(std::chrono::milliseconds(500)); + mouseClick(spix::ItemPath("mainWindow/Button_1"), spix::MouseButtons::Right); + wait(std::chrono::milliseconds(500)); auto result = getStringProperty("mainWindow/results", "text"); std::cout << "-------\nResult:\n-------\n" << result << "\n-------" << std::endl; diff --git a/examples/Basic/main.qml b/examples/Basic/main.qml index c13bea1..153f204 100644 --- a/examples/Basic/main.qml +++ b/examples/Basic/main.qml @@ -23,12 +23,34 @@ Window { Button { objectName: "Button_1" text: "Press Me" - onClicked: resultsView.appendText("Button 1 clicked") + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onClicked: + { + if(mouse.button & Qt.RightButton) + resultsView.appendText("Button 1 right clicked") + else + resultsView.appendText("Button 1 clicked") + } + } } Button { objectName: "Button_2" text: "Or Click Me" - onClicked: resultsView.appendText("Button 2 clicked") + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onClicked: + { + if(mouse.button & Qt.RightButton) + resultsView.appendText("Button 2 right clicked") + else + resultsView.appendText("Button 2 clicked") + } + } } } diff --git a/examples/BasicStandalone/CMakeLists.txt b/examples/BasicStandalone/CMakeLists.txt index 79ac740..13dfeb7 100644 --- a/examples/BasicStandalone/CMakeLists.txt +++ b/examples/BasicStandalone/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.12) project(SpixStandaloneExample LANGUAGES CXX) +set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") + set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../../cmake/modules") set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -14,9 +16,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # find_package(Threads REQUIRED) find_package(AnyRPC REQUIRED) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) find_package(Spix CONFIG REQUIRED) add_executable(SpixStandaloneExample "main.cpp" "qml.qrc") target_compile_definitions(SpixStandaloneExample PRIVATE $<$,$>:QT_QML_DEBUG>) -target_link_libraries(SpixStandaloneExample PRIVATE Threads::Threads Qt5::Core Qt5::Quick Spix::Spix) +target_link_libraries(SpixStandaloneExample PRIVATE Threads::Threads Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick Spix::Spix) diff --git a/examples/BasicStandalone/main.qml b/examples/BasicStandalone/main.qml index c13bea1..153f204 100644 --- a/examples/BasicStandalone/main.qml +++ b/examples/BasicStandalone/main.qml @@ -23,12 +23,34 @@ Window { Button { objectName: "Button_1" text: "Press Me" - onClicked: resultsView.appendText("Button 1 clicked") + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onClicked: + { + if(mouse.button & Qt.RightButton) + resultsView.appendText("Button 1 right clicked") + else + resultsView.appendText("Button 1 clicked") + } + } } Button { objectName: "Button_2" text: "Or Click Me" - onClicked: resultsView.appendText("Button 2 clicked") + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onClicked: + { + if(mouse.button & Qt.RightButton) + resultsView.appendText("Button 2 right clicked") + else + resultsView.appendText("Button 2 clicked") + } + } } } diff --git a/examples/GTest/CMakeLists.txt b/examples/GTest/CMakeLists.txt index 4d16159..385bcdf 100644 --- a/examples/GTest/CMakeLists.txt +++ b/examples/GTest/CMakeLists.txt @@ -4,9 +4,11 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") + +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) find_package(GTest REQUIRED) add_executable(SpixGTestExample "main.cpp" "qml.qrc") target_compile_definitions(SpixGTestExample PRIVATE $<$,$>:QT_QML_DEBUG>) -target_link_libraries(SpixGTestExample PRIVATE Qt5::Core Qt5::Quick GTest::GTest Spix) +target_link_libraries(SpixGTestExample PRIVATE Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick GTest::GTest Spix) diff --git a/examples/GTest/main.cpp b/examples/GTest/main.cpp index 72dafdb..0e6f701 100644 --- a/examples/GTest/main.cpp +++ b/examples/GTest/main.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -62,6 +63,8 @@ TEST(GTestExample, BasicUITest) srv->wait(std::chrono::milliseconds(500)); srv->mouseClick(spix::ItemPath("mainWindow/Button_1")); srv->wait(std::chrono::milliseconds(500)); + srv->mouseClick(spix::ItemPath("mainWindow/Button_1"), spix::MouseButtons::Right); + srv->wait(std::chrono::milliseconds(500)); auto result = srv->getStringProperty("mainWindow/results", "text"); @@ -70,7 +73,8 @@ Button 2 clicked Button 2 clicked Button 1 clicked Button 2 clicked -Button 1 clicked)RSLT"; +Button 1 clicked +Button 1 right clicked)RSLT"; EXPECT_EQ(result, expected_result); diff --git a/examples/GTest/main.qml b/examples/GTest/main.qml index c13bea1..153f204 100644 --- a/examples/GTest/main.qml +++ b/examples/GTest/main.qml @@ -23,12 +23,34 @@ Window { Button { objectName: "Button_1" text: "Press Me" - onClicked: resultsView.appendText("Button 1 clicked") + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onClicked: + { + if(mouse.button & Qt.RightButton) + resultsView.appendText("Button 1 right clicked") + else + resultsView.appendText("Button 1 clicked") + } + } } Button { objectName: "Button_2" text: "Or Click Me" - onClicked: resultsView.appendText("Button 2 clicked") + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onClicked: + { + if(mouse.button & Qt.RightButton) + resultsView.appendText("Button 2 right clicked") + else + resultsView.appendText("Button 2 clicked") + } + } } } diff --git a/examples/ListGridView/CMakeLists.txt b/examples/ListGridView/CMakeLists.txt index 1dda3e3..a253032 100644 --- a/examples/ListGridView/CMakeLists.txt +++ b/examples/ListGridView/CMakeLists.txt @@ -4,8 +4,10 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") + +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) add_executable(SpixListGridViewExample "main.cpp" "qml.qrc") target_compile_definitions(SpixListGridViewExample PRIVATE $<$,$>:QT_QML_DEBUG>) -target_link_libraries(SpixListGridViewExample PRIVATE Qt5::Core Qt5::Quick Spix) +target_link_libraries(SpixListGridViewExample PRIVATE Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick Spix) diff --git a/examples/RemoteCtrl/CMakeLists.txt b/examples/RemoteCtrl/CMakeLists.txt index c2224ad..1675293 100644 --- a/examples/RemoteCtrl/CMakeLists.txt +++ b/examples/RemoteCtrl/CMakeLists.txt @@ -4,8 +4,10 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") + +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) add_executable(SpixRemoteCtrlExample "main.cpp" "qml.qrc") target_compile_definitions(SpixRemoteCtrlExample PRIVATE $<$,$>:QT_QML_DEBUG>) -target_link_libraries(SpixRemoteCtrlExample PRIVATE Qt5::Core Qt5::Quick Spix) +target_link_libraries(SpixRemoteCtrlExample PRIVATE Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick Spix) diff --git a/examples/RemoteCtrl/script/testScript.py b/examples/RemoteCtrl/script/testScript.py index 92d3b08..b30fa4c 100644 --- a/examples/RemoteCtrl/script/testScript.py +++ b/examples/RemoteCtrl/script/testScript.py @@ -1,8 +1,11 @@ import xmlrpc.client s = xmlrpc.client.ServerProxy('http://localhost:9000') -print("Available Methods:") -print(s.system.listMethods()) +print("[+] Available Methods:") +for method in s.system.listMethods(): + print("\t- ","{:20s}".format(method), " : ", end="") + print(s.system.methodHelp(method)) + s.mouseClick("mainWindow/Button_1") s.wait(200) s.mouseClick("mainWindow/Button_2") diff --git a/examples/RepeaterLoader/CMakeLists.txt b/examples/RepeaterLoader/CMakeLists.txt index 0b7a544..5df7ef5 100644 --- a/examples/RepeaterLoader/CMakeLists.txt +++ b/examples/RepeaterLoader/CMakeLists.txt @@ -4,8 +4,14 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") + +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) +find_package(GTest REQUIRED) add_executable(SpixRepeaterLoaderExample "main.cpp" "qml.qrc") +add_executable(SpixRepeaterLoaderExampleGTest "main_gtest.cpp" "qml.qrc") target_compile_definitions(SpixRepeaterLoaderExample PRIVATE $<$,$>:QT_QML_DEBUG>) -target_link_libraries(SpixRepeaterLoaderExample PRIVATE Qt5::Core Qt5::Quick Spix) +target_compile_definitions(SpixRepeaterLoaderExampleGTest PRIVATE $<$,$>:QT_QML_DEBUG>) +target_link_libraries(SpixRepeaterLoaderExample PRIVATE Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick Spix) +target_link_libraries(SpixRepeaterLoaderExampleGTest PRIVATE Qt${SPIX_QT_MAJOR}::Core Qt${SPIX_QT_MAJOR}::Quick GTest::GTest Spix) diff --git a/examples/RepeaterLoader/main.cpp b/examples/RepeaterLoader/main.cpp index 878eac6..8d12702 100644 --- a/examples/RepeaterLoader/main.cpp +++ b/examples/RepeaterLoader/main.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include diff --git a/examples/RepeaterLoader/main_gtest.cpp b/examples/RepeaterLoader/main_gtest.cpp new file mode 100644 index 0000000..7806fbf --- /dev/null +++ b/examples/RepeaterLoader/main_gtest.cpp @@ -0,0 +1,108 @@ +/*** + * Copyright (C) Falko Axmann. All rights reserved. + * Licensed under the MIT license. + * See LICENSE.txt file in the project root for full license information. + ****/ + +#include +#include + +#include +#include + +#include +#include +#include + +class SpixGTest; +static SpixGTest* srv; + +class SpixGTest : public spix::TestServer { +public: + SpixGTest(int argc, char* argv[]) + { + m_argc = argc; + m_argv = argv; + } + + int testResult() { return m_result.load(); } + +protected: + int m_argc; + char** m_argv; + std::atomic m_result {0}; + + void executeTest() override + { + srv = this; + ::testing::InitGoogleTest(&m_argc, m_argv); + auto testResult = RUN_ALL_TESTS(); + m_result.store(testResult); + } +}; + +TEST(RepeaterLoaderGTest, RepeaterLoaderTest) +{ + EXPECT_FALSE(srv->existsAndVisible("mainWindow/ItemDisplayLoader/textItem")) + << "Error: Item should not be visible at launch."; + + srv->mouseClick("mainWindow/ItemButtons/Item_0"); + srv->wait(std::chrono::milliseconds(500)); + + EXPECT_TRUE(srv->existsAndVisible("mainWindow/ItemDisplayLoader/textItem")) + << "Error: Item should be displayed after button pressed."; + + // 'ItemButtons' is not required in the path, + // as 'gridItem_0' is unique within 'mainWindow' + srv->mouseClick("mainWindow/Item_1"); + srv->wait(std::chrono::milliseconds(500)); + + EXPECT_STREQ(srv->getStringProperty("mainWindow/ItemDisplayLoader/textItem", "text").c_str(), + "I am a view with a pear. Trust me.") + << "Error: Wrong item displayed."; + + srv->mouseClick("mainWindow/ItemButtons/Item_2"); + srv->wait(std::chrono::milliseconds(500)); + srv->mouseClick("mainWindow/ItemButtons/Item_0"); + srv->wait(std::chrono::milliseconds(500)); + srv->mouseClick("mainWindow/ItemButtons/Item_3"); + srv->wait(std::chrono::milliseconds(100)); + + EXPECT_STREQ(srv->getStringProperty("mainWindow/ItemDisplayLoader/textItem", "text").c_str(), + "I am a view with a cucumber. Trust me.") + << "Error: Wrong item displayed"; + + srv->mouseClick("mainWindow/itemCombo"); + srv->wait(std::chrono::milliseconds(400)); + + srv->enterKey("mainWindow/itemCombo", spix::KeyCodes::Down, 0); // Down + srv->wait(std::chrono::milliseconds(200)); + srv->enterKey("mainWindow/itemCombo", spix::KeyCodes::Down, 0); // Down + srv->wait(std::chrono::milliseconds(200)); + srv->enterKey("mainWindow/itemCombo", spix::KeyCodes::Enter, 0); // Enter + srv->wait(std::chrono::milliseconds(100)); + + EXPECT_STREQ(srv->getStringProperty("mainWindow/ItemDisplayLoader/textItem", "text").c_str(), + "I am a view with a banana. Trust me.") + << "Error: Wrong item displayed"; + + srv->quit(); +} + +int main(int argc, char* argv[]) +{ + // Init Qt Qml Application + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; + + // Instantiate and run tests + SpixGTest tests(argc, argv); + auto bot = new spix::QtQmlBot(); + bot->runTestServer(tests); + + app.exec(); + return tests.testResult(); +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index ccd1859..723de5e 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -8,14 +8,14 @@ include(CMakePackageConfigHelpers) # Dependencies # find_package(Threads REQUIRED) -find_package(Qt5 +find_package(AnyRPC REQUIRED) +find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Gui Quick REQUIRED) -find_package(AnyRPC REQUIRED) - +message("Building Spix for Qt ${SPIX_QT_MAJOR}") # # Sources @@ -106,7 +106,7 @@ source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX source FILES ${SOURCES}) # # Qt MOC Files # -qt5_wrap_cpp(MOC_FILES "include/Spix/QtQmlBot.h") +cmake_language(CALL "qt${SPIX_QT_MAJOR}_wrap_cpp" MOC_FILES "include/Spix/QtQmlBot.h") # @@ -124,9 +124,10 @@ target_include_directories(Spix target_link_libraries(Spix PUBLIC Threads::Threads - Qt5::Core - Qt5::Gui - Qt5::Quick + Qt${SPIX_QT_MAJOR}::Core + Qt${SPIX_QT_MAJOR}::Gui + Qt${SPIX_QT_MAJOR}::Quick + PRIVATE AnyRPC::anyrpc ) diff --git a/lib/include/Spix/Events/Identifiers.h b/lib/include/Spix/Events/Identifiers.h new file mode 100644 index 0000000..a5c655d --- /dev/null +++ b/lib/include/Spix/Events/Identifiers.h @@ -0,0 +1,169 @@ +/*** + * Copyright (C) Falko Axmann. All rights reserved. + * Licensed under the MIT license. + * See LICENSE.txt file in the project root for full license information. + ****/ + +#pragma once + +namespace spix { + +struct KeyCodes { + enum : int + { + Escape = 0x01000000, + Tab = 0x01000001, + Backtab = 0x01000002, + Backspace = 0x01000003, + Return = 0x01000004, + Enter = 0x01000005, + Insert = 0x01000006, + Delete = 0x01000007, + Pause = 0x01000008, + Print = 0x01000009, + SysReq = 0x0100000a, + Clear = 0x0100000b, + Home = 0x01000010, + End = 0x01000011, + Left = 0x01000012, + Up = 0x01000013, + Right = 0x01000014, + Down = 0x01000015, + PageUp = 0x01000016, + PageDown = 0x01000017, + Shift = 0x01000020, + Control = 0x01000021, + Meta = 0x01000022, + Alt = 0x01000023, + CapsLock = 0x01000024, + NumLock = 0x01000025, + ScrollLock = 0x01000026, + F1 = 0x01000030, + F2 = 0x01000031, + F3 = 0x01000032, + F4 = 0x01000033, + F5 = 0x01000034, + F6 = 0x01000035, + F7 = 0x01000036, + F8 = 0x01000037, + F9 = 0x01000038, + F10 = 0x01000039, + F11 = 0x0100003a, + F12 = 0x0100003b, + F13 = 0x0100003c, + F14 = 0x0100003d, + F15 = 0x0100003e, + F16 = 0x0100003f, + F17 = 0x01000040, + F18 = 0x01000041, + F19 = 0x01000042, + F20 = 0x01000043, + F21 = 0x01000044, + F22 = 0x01000045, + F23 = 0x01000046, + F24 = 0x01000047, + F25 = 0x01000048, + F26 = 0x01000049, + F27 = 0x0100004a, + F28 = 0x0100004b, + F29 = 0x0100004c, + F30 = 0x0100004d, + F31 = 0x0100004e, + F32 = 0x0100004f, + F33 = 0x01000050, + F34 = 0x01000051, + F35 = 0x01000052, + Super_L = 0x01000053, + Super_R = 0x01000054, + Menu = 0x01000055, + Hyper_L = 0x01000056, + Hyper_R = 0x01000057, + Help = 0x01000058, + Direction_L = 0x01000059, + Direction_R = 0x01000060, + Space = 0x20, + Char_Exclam = 0x21, + Char_QuoteDbl = 0x22, + Char_NumberSign = 0x23, + Char_Dollar = 0x24, + Char_Percent = 0x25, + Char_Ampersand = 0x26, + Char_Apostrophe = 0x27, + Char_ParenLeft = 0x28, + Char_ParenRight = 0x29, + Char_Asterisk = 0x2a, + Char_Plus = 0x2b, + Char_Comma = 0x2c, + Char_Minus = 0x2d, + Char_Period = 0x2e, + Char_Slash = 0x2f, + Num_0 = 0x30, + Num_1 = 0x31, + Num_2 = 0x32, + Num_3 = 0x33, + Num_4 = 0x34, + Num_5 = 0x35, + Num_6 = 0x36, + Num_7 = 0x37, + Num_8 = 0x38, + Num_9 = 0x39, + Char_Colon = 0x3a, + Char_Semicolon = 0x3b, + Char_Less = 0x3c, + Char_Equal = 0x3d, + Char_Greater = 0x3e, + Char_Question = 0x3f, + Char_At = 0x40, + Char_A = 0x41, + Char_B = 0x42, + Char_C = 0x43, + Char_D = 0x44, + Char_E = 0x45, + Char_F = 0x46, + Char_G = 0x47, + Char_H = 0x48, + Char_I = 0x49, + Char_J = 0x4a, + Char_K = 0x4b, + Char_L = 0x4c, + Char_M = 0x4d, + Char_N = 0x4e, + Char_O = 0x4f, + Char_P = 0x50, + Char_Q = 0x51, + Char_R = 0x52, + Char_S = 0x53, + Char_T = 0x54, + Char_U = 0x55, + Char_V = 0x56, + Char_W = 0x57, + Char_X = 0x58, + Char_Y = 0x59, + Char_Z = 0x5a + }; +}; + +using KeyModifier = unsigned; +struct KeyModifiers { + enum : KeyModifier + { + None = 0, + Shift = 1 << 0, + Control = 1 << 1, + Alt = 1 << 2, + Meta = 1 << 3 + }; +}; + +using MouseButton = unsigned; +struct MouseButtons { + enum : MouseButton + { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Middle = 1 << 2 + }; +}; + +} // namespace spix diff --git a/lib/include/Spix/Events/KeyCodes.h b/lib/include/Spix/Events/KeyCodes.h deleted file mode 100644 index 8c4ad40..0000000 --- a/lib/include/Spix/Events/KeyCodes.h +++ /dev/null @@ -1,143 +0,0 @@ -/*** - * Copyright (C) Falko Axmann. All rights reserved. - * Licensed under the MIT license. - * See LICENSE.txt file in the project root for full license information. - ****/ - -#pragma once - -namespace spix { - -struct KeyCodes { - static const int Escape = 0x01000000; - static const int Tab = 0x01000001; - static const int Backtab = 0x01000002; - static const int Backspace = 0x01000003; - static const int Return = 0x01000004; - static const int Enter = 0x01000005; - static const int Insert = 0x01000006; - static const int Delete = 0x01000007; - static const int Pause = 0x01000008; - static const int Print = 0x01000009; - static const int SysReq = 0x0100000a; - static const int Clear = 0x0100000b; - static const int Home = 0x01000010; - static const int End = 0x01000011; - static const int Left = 0x01000012; - static const int Up = 0x01000013; - static const int Right = 0x01000014; - static const int Down = 0x01000015; - static const int PageUp = 0x01000016; - static const int PageDown = 0x01000017; - static const int Shift = 0x01000020; - static const int Control = 0x01000021; - static const int Meta = 0x01000022; - static const int Alt = 0x01000023; - static const int CapsLock = 0x01000024; - static const int NumLock = 0x01000025; - static const int ScrollLock = 0x01000026; - static const int F1 = 0x01000030; - static const int F2 = 0x01000031; - static const int F3 = 0x01000032; - static const int F4 = 0x01000033; - static const int F5 = 0x01000034; - static const int F6 = 0x01000035; - static const int F7 = 0x01000036; - static const int F8 = 0x01000037; - static const int F9 = 0x01000038; - static const int F10 = 0x01000039; - static const int F11 = 0x0100003a; - static const int F12 = 0x0100003b; - static const int F13 = 0x0100003c; - static const int F14 = 0x0100003d; - static const int F15 = 0x0100003e; - static const int F16 = 0x0100003f; - static const int F17 = 0x01000040; - static const int F18 = 0x01000041; - static const int F19 = 0x01000042; - static const int F20 = 0x01000043; - static const int F21 = 0x01000044; - static const int F22 = 0x01000045; - static const int F23 = 0x01000046; - static const int F24 = 0x01000047; - static const int F25 = 0x01000048; - static const int F26 = 0x01000049; - static const int F27 = 0x0100004a; - static const int F28 = 0x0100004b; - static const int F29 = 0x0100004c; - static const int F30 = 0x0100004d; - static const int F31 = 0x0100004e; - static const int F32 = 0x0100004f; - static const int F33 = 0x01000050; - static const int F34 = 0x01000051; - static const int F35 = 0x01000052; - static const int Super_L = 0x01000053; - static const int Super_R = 0x01000054; - static const int Menu = 0x01000055; - static const int Hyper_L = 0x01000056; - static const int Hyper_R = 0x01000057; - static const int Help = 0x01000058; - static const int Direction_L = 0x01000059; - static const int Direction_R = 0x01000060; - static const int Space = 0x20; - static const int Char_Exclam = 0x21; - static const int Char_QuoteDbl = 0x22; - static const int Char_NumberSign = 0x23; - static const int Char_Dollar = 0x24; - static const int Char_Percent = 0x25; - static const int Char_Ampersand = 0x26; - static const int Char_Apostrophe = 0x27; - static const int Char_ParenLeft = 0x28; - static const int Char_ParenRight = 0x29; - static const int Char_Asterisk = 0x2a; - static const int Char_Plus = 0x2b; - static const int Char_Comma = 0x2c; - static const int Char_Minus = 0x2d; - static const int Char_Period = 0x2e; - static const int Char_Slash = 0x2f; - static const int Num_0 = 0x30; - static const int Num_1 = 0x31; - static const int Num_2 = 0x32; - static const int Num_3 = 0x33; - static const int Num_4 = 0x34; - static const int Num_5 = 0x35; - static const int Num_6 = 0x36; - static const int Num_7 = 0x37; - static const int Num_8 = 0x38; - static const int Num_9 = 0x39; - static const int Char_Colon = 0x3a; - static const int Char_Semicolon = 0x3b; - static const int Char_Less = 0x3c; - static const int Char_Equal = 0x3d; - static const int Char_Greater = 0x3e; - static const int Char_Question = 0x3f; - static const int Char_At = 0x40; - static const int Char_A = 0x41; - static const int Char_B = 0x42; - static const int Char_C = 0x43; - static const int Char_D = 0x44; - static const int Char_E = 0x45; - static const int Char_F = 0x46; - static const int Char_G = 0x47; - static const int Char_H = 0x48; - static const int Char_I = 0x49; - static const int Char_J = 0x4a; - static const int Char_K = 0x4b; - static const int Char_L = 0x4c; - static const int Char_M = 0x4d; - static const int Char_N = 0x4e; - static const int Char_O = 0x4f; - static const int Char_P = 0x50; - static const int Char_Q = 0x51; - static const int Char_R = 0x52; - static const int Char_S = 0x53; - static const int Char_T = 0x54; - static const int Char_U = 0x55; - static const int Char_V = 0x56; - static const int Char_W = 0x57; - static const int Char_X = 0x58; - static const int Char_Y = 0x59; - static const int Char_Z = 0x5a; -}; - -} // namespace spix diff --git a/lib/include/Spix/TestServer.h b/lib/include/Spix/TestServer.h index b862837..65bd32b 100644 --- a/lib/include/Spix/TestServer.h +++ b/lib/include/Spix/TestServer.h @@ -13,6 +13,7 @@ #include #include +#include #include @@ -45,6 +46,7 @@ class SPIX_EXPORT TestServer { // Commands void wait(std::chrono::milliseconds waitTime); void mouseClick(ItemPath path); + void mouseClick(ItemPath path, MouseButton mouseButton); void mouseBeginDrag(ItemPath path); void mouseEndDrag(ItemPath path); void mouseDropUrls(ItemPath path, const std::vector& urls); diff --git a/lib/src/AnyRpcServer.cpp b/lib/src/AnyRpcServer.cpp index 735365c..7e275f4 100644 --- a/lib/src/AnyRpcServer.cpp +++ b/lib/src/AnyRpcServer.cpp @@ -24,52 +24,79 @@ AnyRpcServer::AnyRpcServer(int anyrpcPort) anyrpc::MethodManager* methodManager = m_pimpl->server->GetMethodManager(); - utils::AddFunctionToAnyRpc(methodManager, "wait", "Wait given number of milliseconds", + utils::AddFunctionToAnyRpc(methodManager, "wait", + "Wait given number of milliseconds | wait(int millisecondsToWait)", [this](int ms) { wait(std::chrono::milliseconds(ms)); }); - utils::AddFunctionToAnyRpc(methodManager, "mouseClick", "Click on the object at the given path", + utils::AddFunctionToAnyRpc(methodManager, "mouseClick", + "Click on the object at the given path | mouseClick(string path)", [this](std::string path) { mouseClick(std::move(path)); }); - utils::AddFunctionToAnyRpc(methodManager, "mouseBeginDrag", "Begin a drag with the mouse", + + utils::AddFunctionToAnyRpc(methodManager, "mouseClickWithButton", + "Click on the object at the given path with the given mouse button | mouseClickWithButton(string path, int " + "mouseButton)", + [this](std::string path, int mouseButton) { mouseClick(std::move(path), mouseButton); }); + + utils::AddFunctionToAnyRpc(methodManager, "mouseBeginDrag", + "Begin a drag with the mouse | mouseBeginDrag(string path)", [this](std::string path) { mouseBeginDrag(std::move(path)); }); - utils::AddFunctionToAnyRpc(methodManager, "mouseEndDrag", "End a drag with the mouse", + + utils::AddFunctionToAnyRpc(methodManager, "mouseEndDrag", + "End a drag with the mouse | mouseEndDrag(string path)", [this](std::string path) { mouseEndDrag(std::move(path)); }); + utils::AddFunctionToAnyRpc)>(methodManager, "mouseDropUrls", - "Drop Urls with mouse", + "Drop Urls with mouse | mouseDropUrls(string path, [string url1, ...])", [this](std::string path, std::vector urls) { mouseDropUrls(std::move(path), urls); }); + utils::AddFunctionToAnyRpc(methodManager, "inputText", - "Enter text. Events are sent to the item's window at path.", + "Enter text. Events are sent to the item's window at path. | inputText(string path, string text)", [this](std::string path, std::string text) { inputText(std::move(path), std::move(text)); }); - utils::AddFunctionToAnyRpc(methodManager, "enterKey", "Press and release a key", + + utils::AddFunctionToAnyRpc(methodManager, "enterKey", + "Press and release a key | enterKey(string path, int keyCode, unsigned int keyModifier)", [this](std::string path, int keyCode, unsigned modifiers) { enterKey(std::move(path), keyCode, modifiers); }); utils::AddFunctionToAnyRpc(methodManager, "getStringProperty", - "Return a property as string", [this](std::string path, std::string property) { + "Return a property as string | getStringProperty(string path, string property) : string property_value", + [this](std::string path, std::string property) { return getStringProperty(std::move(path), std::move(property)); }); + utils::AddFunctionToAnyRpc(methodManager, "setStringProperty", - "Set the string value of the given property", + "Set the string value of the given property | setStringProperty(string path, string property, string " + "new_value)", [this](std::string path, std::string property, std::string value) { setStringProperty(std::move(path), std::move(property), std::move(value)); }); + utils::AddFunctionToAnyRpc(std::string)>(methodManager, "getBoundingBox", - "Return the bounding box of an item in screen coordinates", [this](std::string path) { + "Return the bounding box of an item in screen coordinates | getBoundingBox(string path) : (doubles) " + "[topLeft.x, topLeft.y , width, height]", + [this](std::string path) { auto bounds = getBoundingBox(std::move(path)); return std::vector {bounds.topLeft.x, bounds.topLeft.y, bounds.size.width, bounds.size.height}; }); + utils::AddFunctionToAnyRpc(methodManager, "existsAndVisible", - "Returns true if the given object exists", + "Returns true if the given object exists | existsAndVisible(string path) : bool exists_and_visible", [this](std::string path) { return existsAndVisible(std::move(path)); }); + utils::AddFunctionToAnyRpc()>(methodManager, "getErrors", - "Returns internal errors that occurred during test execution", [this]() { return getErrors(); }); + "Returns internal errors that occurred during test execution | getErrors() : (strings) [error1, ...]", + [this]() { return getErrors(); }); utils::AddFunctionToAnyRpc(methodManager, "takeScreenshot", - "Take a screenshot of the object and save it as a file", [this](std::string targetItem, std::string filePath) { + "Take a screenshot of the object and save it as a file | takeScreenshot(string pathToTargetedItem, string " + "filePath)", + [this](std::string targetItem, std::string filePath) { return takeScreenshot(std::move(targetItem), std::move(filePath)); }); - utils::AddFunctionToAnyRpc(methodManager, "quit", "Close the app", [this] { quit(); }); + utils::AddFunctionToAnyRpc(methodManager, "quit", "Close the app | quit()", [this] { quit(); }); - utils::AddFunctionToAnyRpc(methodManager, "command", "Executes a generic command", + utils::AddFunctionToAnyRpc(methodManager, "command", + "Executes a generic command | command(string command, string payload)", [this](std::string command, std::string payload) { genericCommand(command, payload); }); m_pimpl->server->BindAndListen(anyrpcPort); diff --git a/lib/src/Commands/ClickOnItem.cpp b/lib/src/Commands/ClickOnItem.cpp index 3bafdda..8cbc570 100644 --- a/lib/src/Commands/ClickOnItem.cpp +++ b/lib/src/Commands/ClickOnItem.cpp @@ -11,8 +11,9 @@ namespace spix { namespace cmd { -ClickOnItem::ClickOnItem(ItemPosition path) +ClickOnItem::ClickOnItem(ItemPosition path, MouseButton mouseButton) : m_position(std::move(path)) +, m_mouseButton(mouseButton) { } @@ -28,8 +29,8 @@ void ClickOnItem::execute(CommandEnvironment& env) auto size = item->size(); auto mousePoint = m_position.positionForItemSize(size); - env.scene().events().mouseDown(item.get(), mousePoint, Events::MouseButtons::left); - env.scene().events().mouseUp(item.get(), mousePoint, Events::MouseButtons::left); + env.scene().events().mouseDown(item.get(), mousePoint, m_mouseButton); + env.scene().events().mouseUp(item.get(), mousePoint, m_mouseButton); } } // namespace cmd diff --git a/lib/src/Commands/ClickOnItem.h b/lib/src/Commands/ClickOnItem.h index d13c826..c481997 100644 --- a/lib/src/Commands/ClickOnItem.h +++ b/lib/src/Commands/ClickOnItem.h @@ -9,6 +9,7 @@ #include #include "Command.h" +#include #include namespace spix { @@ -16,12 +17,13 @@ namespace cmd { class SPIX_EXPORT ClickOnItem : public Command { public: - ClickOnItem(ItemPosition path); + ClickOnItem(ItemPosition path, MouseButton mouseButton); void execute(CommandEnvironment& env) override; private: ItemPosition m_position; + MouseButton m_mouseButton; }; } // namespace cmd diff --git a/lib/src/Commands/DragBegin.cpp b/lib/src/Commands/DragBegin.cpp index ea74b56..f1c4665 100644 --- a/lib/src/Commands/DragBegin.cpp +++ b/lib/src/Commands/DragBegin.cpp @@ -27,7 +27,7 @@ void DragBegin::execute(CommandEnvironment& env) auto size = item->size(); Point midPoint(size.width / 2.0, size.height / 2.0); - env.scene().events().mouseDown(item.get(), midPoint, Events::MouseButtons::left); + env.scene().events().mouseDown(item.get(), midPoint, MouseButtons::Left); env.scene().events().mouseMove(item.get(), midPoint); } diff --git a/lib/src/Commands/DragEnd.cpp b/lib/src/Commands/DragEnd.cpp index 351734d..f8f1073 100644 --- a/lib/src/Commands/DragEnd.cpp +++ b/lib/src/Commands/DragEnd.cpp @@ -28,7 +28,7 @@ void DragEnd::execute(CommandEnvironment& env) auto size = item->size(); Point midPoint(size.width / 2.0, size.height / 2.0); env.scene().events().mouseMove(item.get(), midPoint); - env.scene().events().mouseUp(item.get(), midPoint, Events::MouseButtons::left); + env.scene().events().mouseUp(item.get(), midPoint, MouseButtons::Left); } } // namespace cmd diff --git a/lib/src/Commands/EnterKey.cpp b/lib/src/Commands/EnterKey.cpp index 0b5b782..58ca297 100644 --- a/lib/src/Commands/EnterKey.cpp +++ b/lib/src/Commands/EnterKey.cpp @@ -11,7 +11,7 @@ namespace spix { namespace cmd { -EnterKey::EnterKey(ItemPath path, int keyCode, Events::KeyModifier mod) +EnterKey::EnterKey(ItemPath path, int keyCode, KeyModifier mod) : m_path(std::move(path)) , m_keyCode(keyCode) , m_mod(mod) diff --git a/lib/src/Commands/EnterKey.h b/lib/src/Commands/EnterKey.h index dbe49f4..1c8720e 100644 --- a/lib/src/Commands/EnterKey.h +++ b/lib/src/Commands/EnterKey.h @@ -15,14 +15,14 @@ namespace cmd { class EnterKey : public Command { public: - EnterKey(ItemPath path, int keyCode, Events::KeyModifier mod); + EnterKey(ItemPath path, int keyCode, KeyModifier mod); void execute(CommandEnvironment& env) override; private: ItemPath m_path; int m_keyCode; - Events::KeyModifier m_mod; + KeyModifier m_mod; }; } // namespace cmd diff --git a/lib/src/Scene/Events.h b/lib/src/Scene/Events.h index 34df6a0..b37fc91 100644 --- a/lib/src/Scene/Events.h +++ b/lib/src/Scene/Events.h @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -16,23 +17,6 @@ namespace spix { class Events { public: - struct MouseButtons { - static const unsigned none = 0; - static const unsigned left = 1 << 0; - static const unsigned right = 1 << 1; - static const unsigned middle = 1 << 2; - }; - using MouseButton = unsigned; - - struct KeyModifiers { - static const unsigned none = 0; - static const unsigned shift = 1 << 0; - static const unsigned control = 1 << 1; - static const unsigned alt = 1 << 2; - static const unsigned meta = 1 << 3; - }; - using KeyModifier = unsigned; - virtual ~Events() = default; virtual void mouseDown(Item* item, Point loc, MouseButton button) = 0; diff --git a/lib/src/Scene/Qt/QtEvents.cpp b/lib/src/Scene/Qt/QtEvents.cpp index 1d17447..449b901 100644 --- a/lib/src/Scene/Qt/QtEvents.cpp +++ b/lib/src/Scene/Qt/QtEvents.cpp @@ -37,46 +37,46 @@ QQuickWindow* getWindowAndPositionForItem(Item* item, Point relToItemPos, QPoint return window; } -Qt::MouseButton getQtMouseButtonValue(Events::MouseButton button) +Qt::MouseButton getQtMouseButtonValue(MouseButton button) { unsigned qtButton = Qt::MouseButton::NoButton; - if (button & Events::MouseButtons::left) { + if (button & MouseButtons::Left) { qtButton |= Qt::MouseButton::LeftButton; } - if (button & Events::MouseButtons::right) { + if (button & MouseButtons::Right) { qtButton |= Qt::MouseButton::RightButton; } - if (button & Events::MouseButtons::middle) { + if (button & MouseButtons::Middle) { qtButton |= Qt::MouseButton::MiddleButton; } return static_cast(qtButton); } -Qt::KeyboardModifiers getQtKeyboardModifiers(QtEvents::KeyModifier mod) +Qt::KeyboardModifiers getQtKeyboardModifiers(KeyModifier mod) { Qt::KeyboardModifiers qtmod = Qt::NoModifier; - if (mod & QtEvents::KeyModifiers::shift) { + if (mod & KeyModifiers::Shift) { qtmod = qtmod | Qt::ShiftModifier; } - if (mod & QtEvents::KeyModifiers::control) { + if (mod & KeyModifiers::Control) { qtmod = qtmod | Qt::ControlModifier; } - if (mod & QtEvents::KeyModifiers::alt) { + if (mod & KeyModifiers::Alt) { qtmod = qtmod | Qt::AltModifier; } - if (mod & QtEvents::KeyModifiers::meta) { + if (mod & KeyModifiers::Meta) { qtmod = qtmod | Qt::MetaModifier; } return qtmod; } -void sendQtKeyEvent(Item* item, bool press, int keyCode, QtEvents::KeyModifier mod) +void sendQtKeyEvent(Item* item, bool press, int keyCode, KeyModifier mod) { auto qtitem = dynamic_cast(item); if (!qtitem) @@ -102,8 +102,8 @@ void QtEvents::mouseDown(Item* item, Point loc, MouseButton button) Qt::MouseButton eventCausingButton = getQtMouseButtonValue(button); Qt::MouseButtons activeButtons = getQtMouseButtonValue(m_pressedMouseButtons); - QMouseEvent* event = new QMouseEvent( - QEvent::MouseButtonPress, windowLoc, eventCausingButton, activeButtons, Qt::KeyboardModifier()); + QMouseEvent* event + = new QMouseEvent(QEvent::MouseButtonPress, windowLoc, eventCausingButton, activeButtons, Qt::NoModifier); QGuiApplication::postEvent(window, event); } @@ -114,12 +114,20 @@ void QtEvents::mouseUp(Item* item, Point loc, MouseButton button) if (!window) return; +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + // Qt6 expects the mouse to be down during the event + Qt::MouseButton eventCausingButton = getQtMouseButtonValue(button); + Qt::MouseButtons activeButtons = getQtMouseButtonValue(m_pressedMouseButtons); + m_pressedMouseButtons ^= button; +#else + // Qt5 expects the mouse to be up during the event m_pressedMouseButtons ^= button; Qt::MouseButton eventCausingButton = getQtMouseButtonValue(button); Qt::MouseButtons activeButtons = getQtMouseButtonValue(m_pressedMouseButtons); +#endif - QMouseEvent* event = new QMouseEvent( - QEvent::MouseButtonRelease, windowLoc, eventCausingButton, activeButtons, Qt::KeyboardModifier()); + QMouseEvent* event + = new QMouseEvent(QEvent::MouseButtonRelease, windowLoc, eventCausingButton, activeButtons, Qt::NoModifier); QGuiApplication::postEvent(window, event); } @@ -134,14 +142,14 @@ void QtEvents::mouseMove(Item* item, Point loc) // Wiggle the cursor a bit. This is needed to correctly recognize drag events windowLoc.rx() -= 1; - QMouseEvent* mouseMoveEvent = new QMouseEvent( - QEvent::MouseMove, windowLoc, Qt::MouseButton::NoButton, activeButtons, Qt::KeyboardModifier()); + QMouseEvent* mouseMoveEvent + = new QMouseEvent(QEvent::MouseMove, windowLoc, Qt::MouseButton::NoButton, activeButtons, Qt::NoModifier); QGuiApplication::postEvent(window, mouseMoveEvent); // Wiggle the cursor a bit. This is needed to correctly recognize drag events windowLoc.rx() += 1; - mouseMoveEvent = new QMouseEvent( - QEvent::MouseMove, windowLoc, Qt::MouseButton::NoButton, activeButtons, Qt::KeyboardModifier()); + mouseMoveEvent + = new QMouseEvent(QEvent::MouseMove, windowLoc, Qt::MouseButton::NoButton, activeButtons, Qt::NoModifier); QGuiApplication::postEvent(window, mouseMoveEvent); } @@ -153,8 +161,7 @@ void QtEvents::stringInput(Item* item, const std::string& text) auto window = qtitem->qquickitem()->window(); - auto keyDownEvent - = new QKeyEvent(QEvent::KeyPress, 0 /* key id */, Qt::KeyboardModifier(), QString::fromStdString(text)); + auto keyDownEvent = new QKeyEvent(QEvent::KeyPress, 0 /* key id */, Qt::NoModifier, QString::fromStdString(text)); QGuiApplication::postEvent(window, keyDownEvent); } @@ -189,15 +196,15 @@ void QtEvents::extMouseDrop(Item* item, Point loc, PasteboardContent& content) mimeData->setUrls(urlList); auto enter = new QDragEnterEvent(windowLocInt, Qt::CopyAction | Qt::MoveAction | Qt::LinkAction, mimeData, - Qt::MouseButton::NoButton, Qt::KeyboardModifier()); + Qt::MouseButton::NoButton, Qt::NoModifier); QGuiApplication::postEvent(window, enter); auto move = new QDragMoveEvent(windowLocInt, Qt::CopyAction | Qt::MoveAction | Qt::LinkAction, mimeData, - Qt::MouseButton::NoButton, Qt::KeyboardModifier()); + Qt::MouseButton::NoButton, Qt::NoModifier); QGuiApplication::postEvent(window, move); auto drop = new QDropEvent(windowLoc, Qt::CopyAction | Qt::MoveAction | Qt::LinkAction, mimeData, - Qt::MouseButton::NoButton, Qt::KeyboardModifier()); + Qt::MouseButton::NoButton, Qt::NoModifier); QGuiApplication::postEvent(window, drop); } diff --git a/lib/src/Scene/Qt/QtEvents.h b/lib/src/Scene/Qt/QtEvents.h index 02bb1e6..9cce57b 100644 --- a/lib/src/Scene/Qt/QtEvents.h +++ b/lib/src/Scene/Qt/QtEvents.h @@ -23,7 +23,7 @@ class QtEvents : public Events { private: /// Keep track of which buttons are currently pressed - Events::MouseButton m_pressedMouseButtons = Events::MouseButtons::none; + MouseButton m_pressedMouseButtons = MouseButtons::None; }; } // namespace spix diff --git a/lib/src/TestServer.cpp b/lib/src/TestServer.cpp index c2f9ec9..c2ccc84 100644 --- a/lib/src/TestServer.cpp +++ b/lib/src/TestServer.cpp @@ -24,6 +24,8 @@ #include #include +#include + namespace spix { TestServer::~TestServer() @@ -63,7 +65,12 @@ void TestServer::wait(std::chrono::milliseconds waitTime) void TestServer::mouseClick(ItemPath path) { - m_cmdExec->enqueueCommand(path); + m_cmdExec->enqueueCommand(path, spix::MouseButtons::Left); +} + +void TestServer::mouseClick(ItemPath path, MouseButton mouseButton) +{ + m_cmdExec->enqueueCommand(path, mouseButton); } void TestServer::mouseBeginDrag(ItemPath path) diff --git a/lib/tests/CMakeLists.txt b/lib/tests/CMakeLists.txt index 98342be..1bc3c25 100644 --- a/lib/tests/CMakeLists.txt +++ b/lib/tests/CMakeLists.txt @@ -1,4 +1,5 @@ find_package(GTest REQUIRED) +find_package(AnyRPC REQUIRED) set(TEST_SOURCES unittests_main.cpp @@ -20,5 +21,5 @@ source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX source FILES ${TEST_SOURCES add_executable(Spix_test ${TEST_SOURCES}) target_include_directories(Spix_test PRIVATE ../src) -target_link_libraries(Spix_test Spix GTest::GTest) +target_link_libraries(Spix_test Spix AnyRPC::anyrpc GTest::GTest) add_test( NAME SpixUnitTests COMMAND Spix_test ) diff --git a/lib/tests/unittests/Commands/ClickOnItem_test.cpp b/lib/tests/unittests/Commands/ClickOnItem_test.cpp index 6dfea45..cfe845d 100644 --- a/lib/tests/unittests/Commands/ClickOnItem_test.cpp +++ b/lib/tests/unittests/Commands/ClickOnItem_test.cpp @@ -9,11 +9,13 @@ #include #include #include +#include #include TEST(ClickOnItemTest, ErrorOnMissingItem) { - auto command = std::make_unique(spix::ItemPosition("window/some/item")); + auto command + = std::make_unique(spix::ItemPosition("window/some/item"), spix::MouseButtons::Left); spix::MockScene scene; spix::CommandExecuter exec; @@ -33,7 +35,8 @@ TEST(ClickOnItemTest, ClickItem) bool didPostClickEvent = false; bool mouseDown = false; - auto command = std::make_unique(spix::ItemPosition("window/some/item")); + auto command + = std::make_unique(spix::ItemPosition("window/some/item"), spix::MouseButtons::Left); spix::MockScene scene; scene.addItemAtPath(spix::Size(100.0, 30.0), "window/some/item");