From eae4e0f4051b29d7bc1da1c0fe4f6c0bf649b4b0 Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 21 Mar 2022 13:04:11 +0200 Subject: [PATCH] tests(StatusInput): add regression test to check for qml output Also improves on the test structure --- .gitignore | 1 + tests/TestControls/CMakeLists.txt | 24 ++++++--- .../StatusQ/TestHelpers/TestUtils.qml | 14 +++++ tests/TestControls/StatusQ/TestHelpers/qmldir | 3 ++ tests/TestControls/TestHelpers/CMakeLists.txt | 12 +++++ .../TestHelpers/MonitorQtOutput.cpp | 52 +++++++++++++++++++ .../TestHelpers/MonitorQtOutput.h | 32 ++++++++++++ tests/TestControls/main.cpp | 7 +++ .../TestControls/tst_test-StatusBaseInput.qml | 52 +++++++++++++++++++ ...atusinput.qml => tst_test-StatusInput.qml} | 47 ++++++++++++++--- tests/readme.md | 22 ++++++++ 11 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 tests/TestControls/StatusQ/TestHelpers/TestUtils.qml create mode 100644 tests/TestControls/StatusQ/TestHelpers/qmldir create mode 100644 tests/TestControls/TestHelpers/CMakeLists.txt create mode 100644 tests/TestControls/TestHelpers/MonitorQtOutput.cpp create mode 100644 tests/TestControls/TestHelpers/MonitorQtOutput.h create mode 100644 tests/TestControls/tst_test-StatusBaseInput.qml rename tests/TestControls/{tst_test-statusinput.qml => tst_test-StatusInput.qml} (68%) create mode 100644 tests/readme.md diff --git a/.gitignore b/.gitignore index 6e51852e..0f64a4a2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ sandbox/qmlcache_loader.cpp doc/html CMakeLists.txt.user .vscode +build/ diff --git a/tests/TestControls/CMakeLists.txt b/tests/TestControls/CMakeLists.txt index dea62f28..c45a108e 100644 --- a/tests/TestControls/CMakeLists.txt +++ b/tests/TestControls/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.5) -project(TestStatusInputWithRegex LANGUAGES CXX) +project(TestControls LANGUAGES CXX) enable_testing() @@ -9,8 +9,8 @@ list(APPEND QML_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/../../src/") set(QML_IMPORT_PATH "${QML_DIRS}" CACHE STRING "Qt Creator extra qml import paths") set(QML2_IMPORT_PATH "${QML_DIRS}" CACHE STRING "Qt Creator extra qml import paths") -find_package(QT NAMES Qt6 Qt5 COMPONENTS QuickTest Qml REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS QuickTest Qml REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS QuickTest Qml Quick REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS QuickTest Qml Quick REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -24,10 +24,20 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # no need to copy around qml test files for shadow builds - just set the respective define add_definitions(-DQUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") -add_executable(TestStatusInputWithRegex main.cpp) -add_test(NAME TestStatusInputWithRegex COMMAND TestStatusInputWithRegex) +add_executable(${PROJECT_NAME} main.cpp) +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) -target_link_libraries(TestStatusInputWithRegex PRIVATE +# TODO: move this to a test helpers library +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_subdirectory(TestHelpers) + +target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::QuickTest - Qt${QT_VERSION_MAJOR}::Qml) + Qt${QT_VERSION_MAJOR}::Qml + Qt${QT_VERSION_MAJOR}::Quick +) diff --git a/tests/TestControls/StatusQ/TestHelpers/TestUtils.qml b/tests/TestControls/StatusQ/TestHelpers/TestUtils.qml new file mode 100644 index 00000000..6c89bc33 --- /dev/null +++ b/tests/TestControls/StatusQ/TestHelpers/TestUtils.qml @@ -0,0 +1,14 @@ +pragma Singleton + +import QtQml 2.14 +import QtTest 1.0 + +QtObject { + + //> Simulate key and wait for side effects + function pressKeyAndWait(test, item, key) { + test.keyClick(key) + + test.waitForRendering(item) + } +} diff --git a/tests/TestControls/StatusQ/TestHelpers/qmldir b/tests/TestControls/StatusQ/TestHelpers/qmldir new file mode 100644 index 00000000..49de0ec4 --- /dev/null +++ b/tests/TestControls/StatusQ/TestHelpers/qmldir @@ -0,0 +1,3 @@ +module StatusQ.TestHelpers + +singleton TestUtils 0.1 TestUtils.qml diff --git a/tests/TestControls/TestHelpers/CMakeLists.txt b/tests/TestControls/TestHelpers/CMakeLists.txt new file mode 100644 index 00000000..d10b0a82 --- /dev/null +++ b/tests/TestControls/TestHelpers/CMakeLists.txt @@ -0,0 +1,12 @@ +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_sources(${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/MonitorQtOutput.h + ${CMAKE_CURRENT_SOURCE_DIR}/MonitorQtOutput.cpp + + ${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt +) diff --git a/tests/TestControls/TestHelpers/MonitorQtOutput.cpp b/tests/TestControls/TestHelpers/MonitorQtOutput.cpp new file mode 100644 index 00000000..49c68945 --- /dev/null +++ b/tests/TestControls/TestHelpers/MonitorQtOutput.cpp @@ -0,0 +1,52 @@ +#include "MonitorQtOutput.h" + +#include +#include + +std::weak_ptr MonitorQtOutput::m_qtMessageOutputForSharing; +std::mutex MonitorQtOutput::m_mutex; + + +MonitorQtOutput::MonitorQtOutput() +{ + // Ensure only one instance registers a handler + // Warning: don't QT's call loger functions inside the critical section + std::unique_lock localLock(m_mutex); + auto globalMsgOut = m_qtMessageOutputForSharing.lock(); + if(!globalMsgOut) { + // Install message handler if not already done + m_thisMessageOutput = std::make_shared(); + m_qtMessageOutputForSharing = m_thisMessageOutput; + qInstallMessageHandler(qtMessageOutput); + } + else { + m_thisMessageOutput = globalMsgOut; + m_start = m_thisMessageOutput->length(); + } +} + +MonitorQtOutput::~MonitorQtOutput() +{ + std::unique_lock localLock(m_mutex); + if(m_thisMessageOutput.use_count() == 1) { + // Last instance, deregister the handler + qInstallMessageHandler(0); + m_thisMessageOutput.reset(); + } +} + +void +MonitorQtOutput::qtMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + std::unique_lock localLock(m_mutex); + auto globalMsgOut = m_qtMessageOutputForSharing.lock(); + assert(globalMsgOut != nullptr); + globalMsgOut->append(msg + '\n'); +} + +QString +MonitorQtOutput::qtOuput() +{ + assert(m_thisMessageOutput->length() >= m_start); + return m_thisMessageOutput->right(m_thisMessageOutput->length() - m_start); +} diff --git a/tests/TestControls/TestHelpers/MonitorQtOutput.h b/tests/TestControls/TestHelpers/MonitorQtOutput.h new file mode 100644 index 00000000..c482f0f6 --- /dev/null +++ b/tests/TestControls/TestHelpers/MonitorQtOutput.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include +#include + +/// +/// \brief Monitor output for tests and declarativelly control message handler availability +/// \todo Check that QML doesn't keep instance between test runs +/// +class MonitorQtOutput : public QQuickItem +{ + Q_OBJECT +public: + MonitorQtOutput(); + ~MonitorQtOutput(); + + Q_INVOKABLE QString qtOuput(); + +signals: + +private: + static void qtMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + // Use it to keep track of qInstallMessageHandler call + static std::weak_ptr m_qtMessageOutputForSharing; + static std::mutex m_mutex; + std::shared_ptr m_thisMessageOutput; + int m_start = 0; +}; diff --git a/tests/TestControls/main.cpp b/tests/TestControls/main.cpp index 8f83f0da..d825604a 100644 --- a/tests/TestControls/main.cpp +++ b/tests/TestControls/main.cpp @@ -1,6 +1,8 @@ #include #include +#include "TestHelpers/MonitorQtOutput.h" + class TestSetup : public QObject { Q_OBJECT @@ -13,7 +15,12 @@ public slots: { // TODO: Workaround until we make StatusQ a CMake library engine->addImportPath("../../src/"); + engine->addImportPath("."); + // TODO: Alternative to not yet supported QML_ELEMENT + qmlRegisterType("StatusQ.TestHelpers", 0, 1, "MonitorQtOutput"); } +private: + MonitorQtOutput _monitorOutput; }; QUICK_TEST_MAIN_WITH_SETUP(TestControls, TestSetup) diff --git a/tests/TestControls/tst_test-StatusBaseInput.qml b/tests/TestControls/tst_test-StatusBaseInput.qml new file mode 100644 index 00000000..af283280 --- /dev/null +++ b/tests/TestControls/tst_test-StatusBaseInput.qml @@ -0,0 +1,52 @@ +import QtQuick 2.0 +import QtTest 1.0 + +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +import StatusQ.TestHelpers 0.1 + +Item { + width: 300 + height: 100 + + StatusBaseInput { + id: statusInput + text: "Control under test" + placeholderText: "Placeholder" + focus: true + } + + TestCase { + id: testCase + name: "CheckQmlWarnings" + + when: windowShown + + // + // Test guards + + function initTestCase() { + } + + function cleanup() { + statusInput.text = "" + } + + // + // Tests + + function test_initial_empty_is_valid() { + mouseClick(statusInput) + // Do some editing + TestUtils.pressKeyAndWait(testCase, statusInput, Qt.Key_B) + TestUtils.pressKeyAndWait(testCase, statusInput, Qt.Key_Left) + TestUtils.pressKeyAndWait(testCase, statusInput, Qt.Key_A) + verify(qtOuput.qtOuput().length === 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`) + } + } + + MonitorQtOutput { + id: qtOuput + } +} diff --git a/tests/TestControls/tst_test-statusinput.qml b/tests/TestControls/tst_test-StatusInput.qml similarity index 68% rename from tests/TestControls/tst_test-statusinput.qml rename to tests/TestControls/tst_test-StatusInput.qml index f8c7a22b..d7b4e943 100644 --- a/tests/TestControls/tst_test-statusinput.qml +++ b/tests/TestControls/tst_test-StatusInput.qml @@ -4,6 +4,8 @@ import QtTest 1.0 import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 +import StatusQ.TestHelpers 0.1 + Item { width: 300 height: 100 @@ -28,6 +30,8 @@ Item { } TestCase { + id: regexTC + name: "RegexValidationTest" when: windowShown @@ -50,9 +54,9 @@ Item { } function test_regex_validation() { - keyClick(Qt.Key_1) + TestUtils.pressKeyAndWait(regexTC, statusInput, Qt.Key_1) verify(statusInput.valid, "Expected valid input") - keyClick(Qt.Key_Ampersand) + TestUtils.pressKeyAndWait(regexTC, statusInput, Qt.Key_Ampersand) verify(!statusInput.valid, "Expected invalid input") } @@ -61,10 +65,10 @@ Item { verify(statusInput.valid, "Expected valid input") verify(statusInput.text.length === 0, "Expected no input") - keyClick(Qt.Key_2) + TestUtils.pressKeyAndWait(regexTC, statusInput, Qt.Key_2) verify(statusInput.valid, "Expected valid input") verify(statusInput.text === "2", "Expect one character") - keyClick(Qt.Key_Ampersand) + TestUtils.pressKeyAndWait(regexTC, statusInput, Qt.Key_Ampersand) verify(statusInput.valid, "Expected invalid input") verify(statusInput.text === "2", "Expect the same input") } @@ -74,12 +78,43 @@ Item { const appendInvalidChars = "#@!*" statusInput.text = "invalid $" + appendInvalidChars - keyClick(Qt.Key_End) + TestUtils.pressKeyAndWait(regexTC, statusInput, Qt.Key_End) verify(!statusInput.valid, "Expected invalid input due to characters not matching") // Delete invalid characters to get a valid text for(let i = 0; i < appendInvalidChars.length; ++i) - keyClick(Qt.Key_Backspace) + TestUtils.pressKeyAndWait(regexTC, statusInput, Qt.Key_Backspace) verify(statusInput.valid, "Expected valid input") } } + + TestCase { + id: qmlWarnTC + + name: "CheckQmlWarnings" + + when: windowShown + + // + // Test guards + + function initTestCase() { + } + + function cleanup() { + statusInput.text = "" + statusInput.validationMode = _defaultValidationMode + } + + // + // Tests + + function test_initial_empty_is_valid() { + mouseClick(statusInput) + verify(qtOuput.qtOuput().length === 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`) + } + } + + MonitorQtOutput { + id: qtOuput + } } diff --git a/tests/readme.md b/tests/readme.md new file mode 100644 index 00000000..15811986 --- /dev/null +++ b/tests/readme.md @@ -0,0 +1,22 @@ +# Readme + +## Developer instructions + +CMake + +```sh +cd StatusQ/tests/TestControls +cmake -B ./build/ -S . +cmake --build ./build/ +./build/TestControls +``` + +QtCreator + +- Open the StatusQ/tests/CMakeLists.txt +- Choose a QT kit to run the tests +- In the `Test Results` panel choose Run All Tests + +## TODO + +- [ ] Consolidate and integrate with https://github.com/status-im/desktop-ui-tests \ No newline at end of file