diff --git a/README.md b/README.md
index c1ec5bace825b5334ed198f96136b08f31f851a2..48349e995c666ffba3b421b41c21959e0e697816 100644
--- a/README.md
+++ b/README.md
@@ -110,7 +110,7 @@ Test results can be viewed with any JUnit compatible viewer. For example, you ca
 
 ```bash
 npm i -g xunit-viewer
-xunit-viewer -r output.junit.xml
+xunit-viewer -r output.junitxml -s -w
 ```
 
 It'll create a `report.html` file that can be opened in a web browser. For more information about `xunit-viewer` please visit the [GitHub repo][xunit-viewer].
diff --git a/cicd/stages/build.yml b/cicd/stages/build.yml
index 74aaa52fa0690226f5137626f072aaaebb18477e..86cb904d4f9a71c4c363812a7becb19e678987ca 100644
--- a/cicd/stages/build.yml
+++ b/cicd/stages/build.yml
@@ -3,23 +3,27 @@
 .build-components: &build-components
   - pushd 3rdparty/qtquickdesigner-components
   - |
-    cmake \
-    -S . \
-    -G Ninja \
-    -DCMAKE_BUILD_TYPE=Release \
-    -DCMAKE_TOOLCHAIN_FILE=${QDS_CI_JOB_QT_PATH}/lib/cmake/Qt6/qt.toolchain.cmake \
-    -DANDROID_SDK_ROOT=${DOCKER_ENV_ANDROID_SDK_ROOT} \
-    -DANDROID_NDK_ROOT=${DOCKER_ENV_ANDROID_NDK_ROOT} \
-    -DQT_HOST_PATH=${DOCKER_ENV_QT_PATH_LINUX_GCC_64} \
-    -DFLOWVIEW_AUTO_QMLDIR=ON \
-    -DCMAKE_INSTALL_PREFIX=${QDS_CI_JOB_QT_PATH}
-  - cmake --build .
-  - cmake --install .
+    for arch in ${QDS_CI_JOB_ARCHS}; do
+      QT_DIR_WITH_ARCH=${DOCKER_ENV_QT_PATH_WITH_VERSION}/${arch}
+      BUILD_PATH=${QDS_CI_JOB_BUILD_PATH}/components/${arch}
+      cmake \
+      -S . \
+      -G Ninja \
+      -B ${BUILD_PATH} \
+      -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_TOOLCHAIN_FILE=${QT_DIR_WITH_ARCH}/lib/cmake/Qt6/qt.toolchain.cmake \
+      -DANDROID_SDK_ROOT=${DOCKER_ENV_ANDROID_SDK_ROOT} \
+      -DANDROID_NDK_ROOT=${DOCKER_ENV_ANDROID_NDK_ROOT} \
+      -DQT_HOST_PATH=${DOCKER_ENV_QT_PATH_LINUX_GCC_64} \
+      -DFLOWVIEW_AUTO_QMLDIR=ON \
+      -DCMAKE_INSTALL_PREFIX=${QT_DIR_WITH_ARCH}
+      cmake --build ${BUILD_PATH} --target install
+      rm -rf ${BUILD_PATH}
+    done
   - popd
 
-.build-android-apps: &build-android-apps
+.build-app: &build-app
   - export GOOGLE_PLAY_APP_VERSION=$(( $(git tag --list | wc -l) +11 ))
-  # where is this magic "$(git tag --list | wc -l) +11" coming from?
   # "git tag --list | wc -l" counts the number of tags in the repository.
   # The "+11" is a magic number which is the last GOOGLE_PLAY_APP_VERSION
   # that was released before starting to add tags to the repository. After
@@ -39,9 +43,11 @@
     -DCMAKE_INSTALL_PREFIX=${QDS_CI_JOB_QT_PATH} \
     -DANDROID_OPENSSL_PATH=${QDS_CI_JOB_OPENSSL_PATH} \
     -DGOOGLE_PLAY_APP_VERSION=${GOOGLE_PLAY_APP_VERSION} \
+    -DQT_ANDROID_BUILD_ALL_ABIS=ON \
+    -DQT_ANDROID_MULTI_ABI_FORWARD_VARS="GOOGLE_PLAY_APP_VERSION;ANDROID_OPENSSL_PATH" \
     -DBUILD_EXAMPLES=OFF
   - cat ${QDS_CI_JOB_BUILD_PATH}/src/dynamic_imports.qml
-  - cmake --build ${QDS_CI_JOB_BUILD_PATH} --target aab
+  - cmake --build ${QDS_CI_JOB_BUILD_PATH} --target ${QDS_CI_JOB_CMAKE_BUILD_TARGET}
 
 .copy-and-sign-apks: &copy-and-sign-apks
   - QDS_CI_KEYSTORE_PATH=$(pwd)/android_release.keystore
@@ -57,68 +63,54 @@
   - |
     if [[ -n ${CI_COMMIT_TAG} ]];
     then
+      QDS_CI_KEYSTORE_PATH=$(pwd)/android_release.keystore
+      echo ${QDS_VAR_KEYSTORE} | base64 -d > ${QDS_CI_KEYSTORE_PATH}
       cp -r ${QDS_CI_JOB_BUILD_PATH}/src/android-build/build/outputs/bundle/release/* ${QDS_CI_JOB_ARTIFACTS_PATH_APP}
-      /usr/bin/jarsigner -keystore ${CI_PROJECT_DIR}/cicd/android/android_release.keystore ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release.aab designviewer -storepass ${QDS_VAR_PASS}
+      /usr/bin/jarsigner -keystore ${QDS_CI_KEYSTORE_PATH} ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release.aab ${QDS_VAR_KEYSTORE_ALIAS} -storepass ${QDS_VAR_PASS}
     fi
 
-build-android:
+.build-common:
   extends: .pipeline_common
   stage: build
-  parallel:
-    matrix:
-      - QDS_CI_JOB_TARGET_ARCH: "arm64_v8a"
-        QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/arm64-v8a"
-      - QDS_CI_JOB_TARGET_ARCH: "armv7"
-        QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/armeabi-v7a"
-      - QDS_CI_JOB_TARGET_ARCH: "x86"
-        QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/x86"
-      - QDS_CI_JOB_TARGET_ARCH: "x86_64"
-        QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/x86_64"
-  variables:
-    QDS_CI_JOB_BUILD_PATH: "${QDS_CI_CACHE_PATH}/${QDS_CI_JOB_TARGET_ARCH}/build"
-    QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_ARCH}
-    QDS_CI_JOB_ARTIFACTS_PATH_APP: ${QDS_CI_JOB_ARTIFACTS_PATH}/app
-    QDS_CI_JOB_ARTIFACTS_PATH_TEST: ${QDS_CI_JOB_ARTIFACTS_PATH}/test
   artifacts:
-    name: qt_ui_viewer-${CI_JOB_ID}-qt${QDS_CI_QT_VERSION}-${QDS_CI_JOB_TARGET_ARCH}
+    name: qt_ui_viewer-${CI_JOB_ID}-qt${QDS_CI_QT_VERSION}-${QDS_CI_JOB_TARGET_PLATFORM}
     expose_as: "build-artifacts"
     paths:
       - ${QDS_CI_ARTIFACTS_PATH}
     expire_in: 1 week
+
+build-android:
+  extends: .build-common
+  variables:
+    QDS_CI_JOB_TARGET_PLATFORM: "android"
+    QDS_CI_JOB_ARCHS: android_arm64_v8a android_armv7 android_x86 android_x86_64
+    QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/arm64-v8a"
+    QDS_CI_JOB_BUILD_PATH: "${CI_PROJECT_DIR}/${QDS_CI_JOB_TARGET_PLATFORM}/build"
+    QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_PLATFORM}
+    QDS_CI_JOB_ARTIFACTS_PATH_APP: ${QDS_CI_JOB_ARTIFACTS_PATH}/app
+    QDS_CI_JOB_ARTIFACTS_PATH_TEST: ${QDS_CI_JOB_ARTIFACTS_PATH}/test
+    QDS_CI_JOB_CMAKE_BUILD_TARGET: "aab"
   script:
     - mkdir -p ${QDS_CI_JOB_ARTIFACTS_PATH_APP} ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}
-    - export QDS_CI_JOB_QT_PATH="${DOCKER_ENV_QT_PATH_WITH_VERSION}/android_${QDS_CI_JOB_TARGET_ARCH}"
+    - export QDS_CI_JOB_QT_PATH="${DOCKER_ENV_QT_PATH_WITH_VERSION}/android_arm64_v8a"
     - *build-components
-    - *build-android-apps
+    - *build-app
     - *copy-and-sign-apks
     - *copy-and-sign-aab
 
 build-desktop:
-  extends: .pipeline_common
-  stage: build
+  extends: .build-common
   variables:
-    QDS_CI_JOB_TARGET_ARCH: "gcc_64"
-    QDS_CI_JOB_BUILD_PATH: "${QDS_CI_CACHE_PATH}/${QDS_CI_JOB_TARGET_ARCH}/build"
-    QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_ARCH}
-  artifacts:
-    name: qt_ui_viewer-${CI_JOB_ID}-qt${QDS_CI_QT_VERSION}-${QDS_CI_JOB_TARGET_ARCH}
-    expose_as: "build-artifacts"
-    paths:
-      - ${QDS_CI_ARTIFACTS_PATH}
-    expire_in: 1 week
+    QDS_CI_JOB_TARGET_PLATFORM: "desktop"
+    QDS_CI_JOB_ARCHS: gcc_64
+    QDS_CI_JOB_BUILD_PATH: "${CI_PROJECT_DIR}/${QDS_CI_JOB_TARGET_PLATFORM}/build"
+    QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_PLATFORM}
+    QDS_CI_JOB_CMAKE_BUILD_TARGET: "all"
   script:
     - mkdir -p ${QDS_CI_JOB_ARTIFACTS_PATH}
-    - export QDS_CI_JOB_QT_PATH="${DOCKER_ENV_QT_PATH_WITH_VERSION}/${QDS_CI_JOB_TARGET_ARCH}"
-    - *build-components
+    - export QDS_CI_JOB_QT_PATH="${DOCKER_ENV_QT_PATH_LINUX_GCC_64}"
     - apt-get update
     - apt-get install -y libpulse-dev
-    - |
-      cmake \
-      -S . \
-      -B ${QDS_CI_JOB_BUILD_PATH} \
-      -G Ninja \
-      -DCMAKE_BUILD_TYPE=Release \
-      -DCMAKE_PREFIX_PATH=${QDS_CI_JOB_QT_PATH} \
-      -DBUILD_EXAMPLES=OFF
-    - cat ${QDS_CI_JOB_BUILD_PATH}/src/dynamic_imports.qml
-    - cmake --build ${QDS_CI_JOB_BUILD_PATH}
+    - *build-components
+    - *build-app
+    - cp -r ${QDS_CI_JOB_BUILD_PATH}/src//qtuiviewer ${QDS_CI_JOB_ARTIFACTS_PATH}
diff --git a/cicd/stages/release.yml b/cicd/stages/release.yml
index e0deaef999bb86d763a81be3e2b9fafc85aa7510..171f8adab7f91d3bef5d6469e3eacf0c29eb61e3 100644
--- a/cicd/stages/release.yml
+++ b/cicd/stages/release.yml
@@ -5,7 +5,7 @@ create-packages:
   rules:
     - if: $CI_COMMIT_TAG
   needs:
-    - job: test-x86_64
+    - job: test-android
       optional: false
       artifacts: true
     - job: build-android
@@ -29,7 +29,7 @@ create-packages:
           cd "${arch}/app"
           tar -czf "${arch}.tar.gz" *
           echo "Uploading ${arch}.tar.gz to GitLab Package Registry"
-          curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file "${arch}.tar.gz" ${QDS_PACKAGE_URL}/qt-ui-viewer-${CI_COMMIT_TAG}-qt${QDS_CI_QT_VERSION}-${arch}.tar.gz
+          curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file "${arch}.tar.gz" ${QDS_PACKAGE_URL}/qt-ui-viewer-${CI_COMMIT_TAG}-qt${QDS_CI_QT_VERSION}-multiarch.tar.gz
           cd ${current_dir}
       done
   artifacts:
diff --git a/cicd/stages/test.yml b/cicd/stages/test.yml
index 1fd17286d5fd738f01f8410cb4f179c369ed857b..f51db76960d153cc424d09a75fdc958cdd1f33f3 100644
--- a/cicd/stages/test.yml
+++ b/cicd/stages/test.yml
@@ -1,7 +1,18 @@
-# todo: run the tests for all architectures
-test-x86_64:
+.test-common:
   stage: test
   extends: .pipeline_common
+  variables:
+    GIT_SUBMODULE_STRATEGY: none
+    QDS_CI_JOB_TEST_RESULTS_PATH: ${CI_PROJECT_DIR}/test
+  artifacts:
+    paths:
+      - ${QDS_CI_JOB_TEST_RESULTS_PATH}/
+    reports:
+      junit: ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
+    expire_in: 1 week
+
+test-android:
+  extends: .test-common
   tags:
     - qds-blade-server-shell
   needs:
@@ -11,9 +22,6 @@ test-x86_64:
     - job: build-desktop
       optional: false
       artifacts: true
-  variables:
-    GIT_SUBMODULE_STRATEGY: none
-    QDS_CI_JOB_TEST_RESULTS_PATH: ${CI_PROJECT_DIR}/test
   script:
     - |
       if [[ ${QDS_CI_SKIP_TESTS} == "false" ]]; then
@@ -35,7 +43,7 @@ test-x86_64:
         /opt/android/emulator/emulator -avd test -no-boot-anim -no-window -no-audio -no-snapshot -accel on & 2>&1 > /dev/null
         sleep 30
       fi
-    - cd ${QDS_CI_ARTIFACTS_PATH}/x86_64/test || exit 1
+    - cd ${QDS_CI_ARTIFACTS_PATH}/android/test || exit 1
     - mkdir -p ${QDS_CI_JOB_TEST_RESULTS_PATH}
     - adb install -r -g android-build-release.apk
     - adb shell pm clear io.qt.qtdesignviewer.test
@@ -71,9 +79,27 @@ test-x86_64:
     - mv output.junit.xml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
     - chmod +x ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh
     - ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
-  artifacts:
-    paths:
-      - ${QDS_CI_JOB_TEST_RESULTS_PATH}/
-    reports:
-      junit: ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
-    expire_in: 1 week
+
+
+# it's not working as of because of the headless mode
+# possible bugs:
+# - https://bugreports.qt.io/browse/QTBUG-124467
+# - https://bugreports.qt.io/browse/QTBUG-126508
+.test-desktop:
+  extends: .test-common
+  needs:
+    - job: build-desktop
+      optional: false
+      artifacts: true
+  script:
+    - |
+      if [[ ${QDS_CI_SKIP_TESTS} == "false" ]]; then
+        echo "Running tests";
+      else
+        echo "Skipping tests";
+        exit 0;
+      fi
+    - cd ${QDS_CI_ARTIFACTS_PATH}/gcc_64 || exit 1
+    - apt update; apt install -y libxcb-cursor0
+    - ./qtuiviewer_test
+    - cp output.junitxml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
diff --git a/src/backend/backend.cpp b/src/backend/backend.cpp
index 78f62a70ed7a021b6a2b097af263a24d6987e768..fe564c38de0f8edaf413a4cf08a05e26460bc659 100644
--- a/src/backend/backend.cpp
+++ b/src/backend/backend.cpp
@@ -173,7 +173,7 @@ void Backend::initProjectManager()
 void Backend::initDsManager()
 {
     qDebug() << "Design Studio Manager thread started. Initializing Design Studio Manager";
-    m_dsManager.reset(new DesignStudioManager(m_settings.deviceUuid(), this));
+    m_dsManager.reset(new DesignStudioManager(m_settings.deviceUuid()));
 
     connect(m_dsManager.get(), &DesignStudioManager::projectReceived, this, &Backend::runProject);
 
@@ -259,8 +259,10 @@ void Backend::runProject(const QString &id, const QByteArray &projectData)
 
         if (!retVal)
             QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectStopped", Q_ARG(QString, id));
-        else
-            QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectRunning", Q_ARG(QString, id));
+        else {
+            QMetaObject::invokeMethod(m_projectManager.get(), "showAppWindow");
+            QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectStarted", Q_ARG(QString, id));
+        }
 
         emit popupClose();
     });
diff --git a/src/backend/dsconnector/ds.cpp b/src/backend/dsconnector/ds.cpp
index ba39bd50bd26659bf347fb1c51380f4aa37518ae..08c24200d58c368d9d7a4c6e2bf05f9d87f0ae05 100644
--- a/src/backend/dsconnector/ds.cpp
+++ b/src/backend/dsconnector/ds.cpp
@@ -19,7 +19,7 @@ using namespace Qt::Literals;
 constexpr auto deviceInfo = "deviceInfo"_L1;
 constexpr auto projectReceivingProgress = "projectReceivingProgress"_L1;
 constexpr auto projectStarting = "projectStarting"_L1;
-constexpr auto projectRunning = "projectRunning"_L1;
+constexpr auto projectStarted = "projectRunning"_L1;
 constexpr auto projectStopped = "projectStopped"_L1;
 constexpr auto projectLogs = "projectLogs"_L1;
 }; // namespace PackageToDesignStudio
@@ -247,9 +247,9 @@ void DesignStudio::sendProjectStarting()
     sendData(PackageToDesignStudio::projectStarting);
 }
 
-void DesignStudio::sendProjectRunning()
+void DesignStudio::sendProjectStarted()
 {
-    sendData(PackageToDesignStudio::projectRunning);
+    sendData(PackageToDesignStudio::projectStarted);
 }
 
 void DesignStudio::sendProjectStopped()
diff --git a/src/backend/dsconnector/ds.h b/src/backend/dsconnector/ds.h
index 069c9880a39c3ae778b64ea909f5080ede9c7fdb..5034d3c1f9ff63073e970a367ca347a1845d5fce 100644
--- a/src/backend/dsconnector/ds.h
+++ b/src/backend/dsconnector/ds.h
@@ -23,7 +23,7 @@ public:
     // Send data
     void sendDeviceInfo();
     void sendProjectStarting();
-    void sendProjectRunning();
+    void sendProjectStarted();
     void sendProjectStopped();
     void sendProjectLogs(const QString &logs);
 
diff --git a/src/backend/dsconnector/dsmanager.cpp b/src/backend/dsconnector/dsmanager.cpp
index 1c1e6028c5af3b51b281d0ced2695ebaa61c0540..9ca02241fcd11f3aab78bd4145a30568383140cb 100644
--- a/src/backend/dsconnector/dsmanager.cpp
+++ b/src/backend/dsconnector/dsmanager.cpp
@@ -9,26 +9,32 @@
 #include <QJsonObject>
 #include <QUdpSocket>
 
-DesignStudioManager::DesignStudioManager(const QString &deviceUuid, QObject *parent)
+DesignStudioManager::DesignStudioManager(const QString &deviceUuid,
+                                         const quint16 port,
+                                         QObject *parent)
     : QObject(parent)
+    , m_port(port)
     , m_deviceUuid(deviceUuid)
     , m_webSocketServer("DesignStudio", QWebSocketServer::NonSecureMode)
 {}
 
-void DesignStudioManager::init()
+bool DesignStudioManager::init()
 {
-    static bool initialized = false;
-    if (initialized) {
-        return;
+    if (m_webSocketServer.isListening()) {
+        qWarning() << "TCP server is already running";
+        return false;
+    }
+
+    if (!m_webSocketServer.listen(QHostAddress::Any, m_port)) {
+        qWarning() << "Failed to start TCP server on port" << m_port;
+        return false;
     }
-    initialized = true;
 
-    m_webSocketServer.listen(QHostAddress::Any, 40000);
     connect(&m_webSocketServer,
             &QWebSocketServer::newConnection,
             this,
             &DesignStudioManager::incomingConnection);
-    qDebug() << "TCP server listening on port 40000";
+    qDebug() << "TCP server listening on port" << m_webSocketServer.serverPort();
 
     m_discoveryTimer.setInterval(10000);
     connect(&m_discoveryTimer, &QTimer::timeout, [this]() {
@@ -51,6 +57,18 @@ void DesignStudioManager::init()
         udpSocket.writeDatagram(datagram, QHostAddress("10.0.2.2"), port);
     });
     m_discoveryTimer.start();
+
+    return true;
+}
+
+QString DesignStudioManager::webSocketServerName() const
+{
+    return m_webSocketServer.serverName();
+}
+
+int DesignStudioManager::webSocketServerPort() const
+{
+    return m_webSocketServer.serverPort();
 }
 
 void DesignStudioManager::incomingConnection()
@@ -64,6 +82,7 @@ void DesignStudioManager::incomingConnection()
                                                      &QObject::deleteLater);
     connect(designStudio.data(), &DesignStudio::disconnected, this, [this, designStudio]() {
         m_designStudios.removeOne(designStudio);
+        emit designStudioDisconnected(designStudio->id(), designStudio->ipv4Addr());
         qDebug() << "Remaining Design Studios:" << m_designStudios.size();
         if (m_designStudios.isEmpty())
             emit allDesignStudiosDisconnected();
@@ -106,11 +125,11 @@ void DesignStudioManager::sendProjectStarting(const QString &id)
     }
 }
 
-void DesignStudioManager::sendProjectRunning(const QString &id)
+void DesignStudioManager::sendProjectStarted(const QString &id)
 {
     for (const auto &designStudio : m_designStudios) {
         if (designStudio->id() == id)
-            designStudio->sendProjectRunning();
+            designStudio->sendProjectStarted();
     }
 }
 
diff --git a/src/backend/dsconnector/dsmanager.h b/src/backend/dsconnector/dsmanager.h
index 661407a4c9c7e9e2f0e680879d3b6e2a715404c7..7c8fa08cb376381ef2638d021794c6cb8844c12d 100644
--- a/src/backend/dsconnector/dsmanager.h
+++ b/src/backend/dsconnector/dsmanager.h
@@ -11,13 +11,17 @@ class DesignStudioManager : public QObject
 {
     Q_OBJECT
 public:
-    explicit DesignStudioManager(const QString &deviceUuid, QObject *parent = nullptr);
+    explicit DesignStudioManager(const QString &deviceUuid,
+                                 const quint16 port = 40000,
+                                 QObject *parent = nullptr);
 
-    void init();
+    bool init();
+    QString webSocketServerName() const;
+    int webSocketServerPort() const;
 
 public slots:
     void sendProjectStarting(const QString &id);
-    void sendProjectRunning(const QString &id);
+    void sendProjectStarted(const QString &id);
     void sendProjectStopped(const QString &id);
     void sendProjectLogs(const QString &id, const QString &logs);
     QString getDesignStudioIp(const QString &id) const;
@@ -28,6 +32,7 @@ private:
     // Network
     QWebSocketServer m_webSocketServer;
     QTimer m_discoveryTimer;
+    const quint16 m_port;
 
     // Connected Design Studios
     QList<QSharedPointer<DesignStudio>> m_designStudios;
diff --git a/src/backend/main.cpp b/src/backend/main.cpp
index 237cac5ef5b9e9c8f6130742d5eab4640c675c42..3c4bc110027bd544fc218c6dff8c3c604804fb66 100644
--- a/src/backend/main.cpp
+++ b/src/backend/main.cpp
@@ -33,8 +33,6 @@
 int main(int argc, char *argv[])
 {
     Logger::instance();
-
-    qDebug() << "Starting Qt Design Viewer";
     qputenv("QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT", "1");
 
     QApplication app(argc, argv);
@@ -52,6 +50,7 @@ int main(int argc, char *argv[])
 #ifdef Q_OS_ANDROID
     view.showMaximized();
 #else
+    QApplication::setWindowIcon(QIcon(QStringLiteral(":/images/appicon.svg")));
     view.show();
 #endif
 
diff --git a/src/backend/projectmanager.cpp b/src/backend/projectmanager.cpp
index 016e710ce2bdcad478c1f14d07b46a5254ba542e..90a1e042850a368fdbb0499a1587e8f64ec0c178 100644
--- a/src/backend/projectmanager.cpp
+++ b/src/backend/projectmanager.cpp
@@ -287,6 +287,8 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
 
     qDebug() << "Setting up the quickWindow";
     m_quickWindow.reset(qobject_cast<QQuickWindow *>(topLevel));
+    m_quickWindow->setVisible(false);
+
     if (m_quickWindow) {
         qDebug() << "Running with incubator controller";
         m_qmlEngine->setIncubationController(m_quickWindow->incubationController());
@@ -306,7 +308,7 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
         view->setResizeMode(QQuickView::SizeViewToRootObject);
         m_quickWindow->setBaseSize(QSize(contentItem->width(), contentItem->height()));
     }
-    showAppWindow();
+
     return true;
 }
 
@@ -344,7 +346,7 @@ void ProjectManager::orientateWindow(Qt::ScreenOrientation orientation)
              << m_quickWindow->width() << Qt::endl
              << "-- Child size: " << childItem->height() << " x " << childItem->width() << Qt::endl
              << "-- Child pos: " << childItem->x() << ", " << childItem->y() << Qt::endl
-             << "-- Child scale: " << childItem->scale();
+             << "-- Child scale: " << childItem->scale() << Qt::endl;
 
     const QSizeF newContentSize = childItem->size().scaled(screenGeometry.size().toSizeF(),
                                                            Qt::AspectRatioMode::KeepAspectRatio);
@@ -369,12 +371,15 @@ void ProjectManager::showAppWindow()
     qDebug("Initializing and showing the QML app window");
 
     QScreen *screen = QGuiApplication::primaryScreen();
-    connect(screen, &QScreen::orientationChanged, this, &ProjectManager::orientateWindow);
+    connect(screen, &QScreen::orientationChanged, this, [this](Qt::ScreenOrientation orientation) {
+        orientateWindow(orientation);
+    });
+
     orientateWindow(screen->orientation());
 
     connect(m_quickWindow.data(), &QQuickWindow::closing, this, [this, screen]() {
         qDebug() << "QML app window is closing";
-        disconnect(screen, &QScreen::orientationChanged, this, &ProjectManager::orientateWindow);
+        disconnect(screen, &QScreen::orientationChanged, this, nullptr);
         emit closingProject(m_sessionId);
         m_sessionId.clear();
     });
@@ -385,11 +390,16 @@ void ProjectManager::showAppWindow()
 
 void ProjectManager::stopProject()
 {
-    if (!m_quickWindow || !m_quickWindow->isVisible())
+    if (!m_quickWindow)
         return;
 
     qDebug("Stopping the QML app window");
     m_quickWindow->close();
+
+    m_qmlComponent.reset();
+    m_qmlEngine.reset();
+    m_quickWindow.reset();
+    m_sessionId.clear();
 }
 
 QString ProjectManager::sessionId() const
diff --git a/src/backend/projectmanager.h b/src/backend/projectmanager.h
index ae12e3cf6ca38d8f2b3d90c2a1ead4646d0c3f82..269552b51543df46d9e87d4039f5521c0fbd1e1e 100644
--- a/src/backend/projectmanager.h
+++ b/src/backend/projectmanager.h
@@ -41,8 +41,11 @@ public slots:
                     const bool autoScaleProject,
                     const QString &sessionId);
     void stopProject();
+    void showAppWindow();
 
 private:
+    friend class TestProjectManager;
+
     // Member variables
     QByteArray m_projectData;
     QString m_projectPath;
@@ -67,11 +70,9 @@ private:
     QString getMainQmlFile(const QString &projectPath, const QString &qmlProjectFileContent);
     QStringList getImportPaths(const QString &projectPath, const QString &qmlProjectFileContent);
     bool isQt6Project(const QString &qmlProjectFileContent);
-    void parseQmlProjectFile(const QString &fileName, QString *mainFile, QStringList *importPaths);
 
     // window management
     void orientateWindow(Qt::ScreenOrientation orientation);
-    void showAppWindow();
 
 signals:
     void closingProject(const QString &sessionId);
diff --git a/src/backend/settings.cpp b/src/backend/settings.cpp
index cb0459635b8144597c123f8f55deb54b501c696e..408d0dbf16b3a3eee248260fd3ff6d6221833fcb 100644
--- a/src/backend/settings.cpp
+++ b/src/backend/settings.cpp
@@ -35,6 +35,18 @@ Settings::Settings()
     }
 }
 
+bool Settings::clearSettings()
+{
+    const auto backup = m_settings;
+    m_settings = QJsonObject();
+    const bool rV = saveSettings();
+    if (rV)
+        applyDefaultSettings();
+    else
+        m_settings = backup;
+    return rV;
+}
+
 void Settings::applyDefaultSettings()
 {
     m_settings["autoScale"] = true;
diff --git a/src/backend/settings.h b/src/backend/settings.h
index eb87bb22cc7544ea9c136543a790a72edcba49ad..739dfd778f6089db4e4fa217775cd99854a0622a 100644
--- a/src/backend/settings.h
+++ b/src/backend/settings.h
@@ -10,6 +10,8 @@ class Settings
 public:
     Settings();
 
+    bool clearSettings();
+
     void setAutoScaleProject(const bool &enabled);
     void setDeviceUuid(const QString &deviceUuid);
     void setKeepScreenOn(const bool &enabled);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 5c840f42ec52fd632dfd1a2e44302d27e6681ada..71d08408603380646ec972a02fe6c1faf0c8b063 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,10 +1,13 @@
 set(TARGET_NAME ${PROJECT_NAME}_test)
 
-find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Multimedia WebSockets)
+find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui Widgets Multimedia WebSockets)
 
 enable_testing(true)
 qt_add_executable(${TARGET_NAME}
     tst_settings.cpp tst_settings.h
+    tst_dsmanager.cpp tst_dsmanager.h
+    tst_projectmanager.cpp tst_projectmanager.h
+    mock_ds.cpp mock_ds.h
     main.cpp
 )
 
@@ -12,10 +15,18 @@ add_test(NAME ${TARGET_NAME} COMMAND qtuiviewer_test)
 target_link_libraries(
     ${TARGET_NAME}
     PRIVATE
-    Qt::Test Qt::Core Qt::Quick Qt::Gui Qt::Multimedia Qt::WebSockets
+    Qt::Test Qt::Core Qt::Quick Qt::Gui Qt::Multimedia Qt::WebSockets Qt::Widgets
     qtuiviewerlib
 )
 
+qt_add_resources(${TARGET_NAME} "resources"
+    PREFIX
+        "/"
+    FILES
+        "data/test_project/test_project_success.qmlrc"
+        "data/test_project/test_project_fail.qmlrc"
+)
+
 set_target_properties(${TARGET_NAME} PROPERTIES QT_ANDROID_PACKAGE_NAME "io.qt.qtdesignviewer.test")
 set_property(TARGET ${TARGET_NAME}
     APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
diff --git a/tests/android/AndroidManifest.xml b/tests/android/AndroidManifest.xml
index 294377be7cc1a00cfcb489e580c19b38562e86d0..440774b83a60da6f13c16189e9fdb4bc379dffba 100644
--- a/tests/android/AndroidManifest.xml
+++ b/tests/android/AndroidManifest.xml
@@ -5,7 +5,7 @@
     <!-- %%INSERT_FEATURES -->
     <supports-screens android:anyDensity="true" android:largeScreens="true"
         android:normalScreens="true" android:smallScreens="true" />
-    <application android:icon="@mipmap/app_icon" android:debuggable="true"
+    <application android:icon="@mipmap/ic_launcher" android:debuggable="true"
         android:name="org.qtproject.qt.android.bindings.QtApplication"
         android:extractNativeLibs="true"
         android:hardwareAccelerated="true" android:label="Qt UI Viewer Test"
diff --git a/tests/android/res/drawable/ic_launcher_background.xml b/tests/android/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e3c2454e06fb6ae5cb0f94109492d3b473966a16
--- /dev/null
+++ b/tests/android/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,26 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group android:scaleX="0.99"
+      android:scaleY="0.99"
+      android:translateX="0.54"
+      android:translateY="0.54">
+    <path
+        android:pathData="M0,13.5V108H94.5L108,94.5V0H13.5L0,13.5Z">
+      <aapt:attr name="android:fillColor">
+        <gradient 
+            android:startX="104.99"
+            android:startY="-26.62"
+            android:endX="11.55"
+            android:endY="126.08"
+            android:type="linear">
+          <item android:offset="0.14" android:color="#FF014B57"/>
+          <item android:offset="1" android:color="#FF0F7080"/>
+        </gradient>
+      </aapt:attr>
+    </path>
+  </group>
+</vector>
diff --git a/tests/android/res/drawable/ic_launcher_foreground.xml b/tests/android/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f09532519c052b3af27d99d52b060696fe593a5d
--- /dev/null
+++ b/tests/android/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group android:scaleX="0.66"
+      android:scaleY="0.66"
+      android:translateX="17.37"
+      android:translateY="17.04">
+    <group>
+      <clip-path
+          android:pathData="M0.25,0.13h107.75v107.75h-107.75z"/>
+      <path
+          android:pathData="M28.84,62.54C28.84,67.29 31.25,69.66 36.08,69.66C40.91,69.66 43.33,67.29 43.33,62.54V35.42H51.59V62.36C51.59,67.35 50.29,71.02 47.69,73.37C45.14,75.69 41.27,76.84 36.08,76.84C30.89,76.84 27,75.69 24.41,73.37C21.85,71.02 20.58,67.35 20.58,62.36V35.42H28.84V62.54ZM82.13,35.42H90.81L81.35,76.13H66.09L56.63,35.42H65.31L72.38,68.94H75.07L82.13,35.42Z"
+          android:fillColor="#2CDE85"/>
+    </group>
+  </group>
+</vector>
diff --git a/tests/android/res/mipmap-anydpi-v26/ic_launcher.xml b/tests/android/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bbd3e021239ce758474da78cfc2ca3cf85ed0d91
--- /dev/null
+++ b/tests/android/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/tests/android/res/mipmap-anydpi-v26/ic_launcher_round.xml b/tests/android/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bbd3e021239ce758474da78cfc2ca3cf85ed0d91
--- /dev/null
+++ b/tests/android/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/tests/android/res/mipmap-hdpi/app_icon.png b/tests/android/res/mipmap-hdpi/app_icon.png
deleted file mode 100644
index 74a188d073456264ab840da9314a48447fc95fcf..0000000000000000000000000000000000000000
Binary files a/tests/android/res/mipmap-hdpi/app_icon.png and /dev/null differ
diff --git a/tests/android/res/mipmap-hdpi/ic_launcher.webp b/tests/android/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..45b059ae192f60f60ed2303339c36c294dbbfc3e
Binary files /dev/null and b/tests/android/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/tests/android/res/mipmap-hdpi/ic_launcher_round.webp b/tests/android/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..6478dcebfebd3dd0c861fa21138c69bc168fffc9
Binary files /dev/null and b/tests/android/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/tests/android/res/mipmap-ldpi/app_icon.png b/tests/android/res/mipmap-ldpi/app_icon.png
deleted file mode 100644
index a52520e2630446955333139ca069431c38b75376..0000000000000000000000000000000000000000
Binary files a/tests/android/res/mipmap-ldpi/app_icon.png and /dev/null differ
diff --git a/tests/android/res/mipmap-mdpi/app_icon.png b/tests/android/res/mipmap-mdpi/app_icon.png
deleted file mode 100644
index 36a356c0cec907702d1ecd5562586ca8ee72325c..0000000000000000000000000000000000000000
Binary files a/tests/android/res/mipmap-mdpi/app_icon.png and /dev/null differ
diff --git a/tests/android/res/mipmap-mdpi/ic_launcher.webp b/tests/android/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..f023c33ba54629232b36c2efda70367edc74dd99
Binary files /dev/null and b/tests/android/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/tests/android/res/mipmap-mdpi/ic_launcher_round.webp b/tests/android/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a3cc2d08d9da07471aba193ffefba9328d3299aa
Binary files /dev/null and b/tests/android/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/tests/android/res/mipmap-xhdpi/app_icon.png b/tests/android/res/mipmap-xhdpi/app_icon.png
deleted file mode 100644
index e1f2e9dc1f2653cdb78f3392df37664615f811aa..0000000000000000000000000000000000000000
Binary files a/tests/android/res/mipmap-xhdpi/app_icon.png and /dev/null differ
diff --git a/tests/android/res/mipmap-xhdpi/ic_launcher.webp b/tests/android/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..0c33762a480bca1d329d2b90252297fdb106b8e6
Binary files /dev/null and b/tests/android/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/tests/android/res/mipmap-xhdpi/ic_launcher_round.webp b/tests/android/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..6599d9b307275916df8e1da8a74c6ad9917fbf02
Binary files /dev/null and b/tests/android/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/tests/android/res/mipmap-xxhdpi/app_icon.png b/tests/android/res/mipmap-xxhdpi/app_icon.png
deleted file mode 100644
index 652a8e9b7e4e7ebb31424805025b1a2bce8e708d..0000000000000000000000000000000000000000
Binary files a/tests/android/res/mipmap-xxhdpi/app_icon.png and /dev/null differ
diff --git a/tests/android/res/mipmap-xxhdpi/ic_launcher.webp b/tests/android/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..bb3c5ea66b3356bf2195068b3dc4a6d8b6466a12
Binary files /dev/null and b/tests/android/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/tests/android/res/mipmap-xxhdpi/ic_launcher_round.webp b/tests/android/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..001d3b0de54aa94df573dc740f0d45ea2e8f245b
Binary files /dev/null and b/tests/android/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/tests/android/res/mipmap-xxxhdpi/app_icon.png b/tests/android/res/mipmap-xxxhdpi/app_icon.png
deleted file mode 100644
index 7a404f6cbb8e0acc99545092319bea31c384ecca..0000000000000000000000000000000000000000
Binary files a/tests/android/res/mipmap-xxxhdpi/app_icon.png and /dev/null differ
diff --git a/tests/android/res/mipmap-xxxhdpi/ic_launcher.webp b/tests/android/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000000000000000000000000000000000..c60e7e17d4c43dfa63500e0bf30802ddfc1fbd46
Binary files /dev/null and b/tests/android/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/tests/android/res/mipmap-xxxhdpi/ic_launcher_round.webp b/tests/android/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a41bd7a2b444bd7108df57d4334afc50bc5ae025
Binary files /dev/null and b/tests/android/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/tests/android/res/values/strings.xml b/tests/android/res/values/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..004d0188e87976edca44fd1c4e4b3cd1c8469571
--- /dev/null
+++ b/tests/android/res/values/strings.xml
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources>
+    <string name="app_name">Qt UI Viewer</string>
+</resources>
diff --git a/tests/data/test_project/Generated/Quick3DAssets.txt b/tests/data/test_project/Generated/Quick3DAssets.txt
new file mode 100644
index 0000000000000000000000000000000000000000..84c843f100d41b805bc1dab082525ad2228526b7
--- /dev/null
+++ b/tests/data/test_project/Generated/Quick3DAssets.txt
@@ -0,0 +1 @@
+Imported 3D assets and components imported from bundles will be created in this folder.
diff --git a/tests/data/test_project/qtquickcontrols2.conf b/tests/data/test_project/qtquickcontrols2.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2ed8ad76d0505e144850b96195cc1aeba3421010
--- /dev/null
+++ b/tests/data/test_project/qtquickcontrols2.conf
@@ -0,0 +1,8 @@
+[Controls]
+Style=Basic
+
+[Material]
+Theme=Light
+
+[Universal]
+Theme=System
diff --git a/tests/data/test_project/temp.qrc b/tests/data/test_project/temp.qrc
new file mode 100644
index 0000000000000000000000000000000000000000..16e081ed0fbbfa80ac705031e03855684fc22121
--- /dev/null
+++ b/tests/data/test_project/temp.qrc
@@ -0,0 +1,13 @@
+
+<RCC>
+<qresource>
+<file>qtquickcontrols2.conf</file>
+<file>test_project.qmlproject</file>
+<file>test_project/Constants.qml</file>
+<file>test_project/EventListModel.qml</file>
+<file>test_project/EventListSimulator.qml</file>
+<file>test_project/qmldir</file>
+<file>test_projectContent/App.qml</file>
+<file>test_projectContent/Screen01.ui.qml</file>
+</qresource>
+</RCC>
\ No newline at end of file
diff --git a/tests/data/test_project/test_project.qmlproject b/tests/data/test_project/test_project.qmlproject
new file mode 100644
index 0000000000000000000000000000000000000000..9d4155129ea4bd710713d68fcd3e3a8ad51d1706
--- /dev/null
+++ b/tests/data/test_project/test_project.qmlproject
@@ -0,0 +1,122 @@
+import QmlProject
+
+Project {
+    mainFile: "test_projectContent/App.qml"
+    mainUiFile: "test_projectContent/Screen01.ui.qml"
+
+    /* Include .qml, .js, and image files from current directory and subdirectories */
+    QmlFiles {
+        directory: "test_project"
+    }
+
+    QmlFiles {
+        directory: "test_projectContent"
+    }
+
+    QmlFiles {
+        directory: "Generated"
+    }
+
+    JavaScriptFiles {
+        directory: "test_project"
+    }
+
+    JavaScriptFiles {
+        directory: "test_projectContent"
+    }
+
+    ImageFiles {
+        directory: "test_projectContent"
+    }
+
+    ImageFiles {
+        directory: "Generated"
+    }
+
+    Files {
+        filter: "*.conf"
+        files: ["qtquickcontrols2.conf"]
+    }
+
+    Files {
+        filter: "qmldir"
+        directory: "."
+    }
+
+    Files {
+        filter: "*.ttf;*.otf"
+    }
+
+    Files {
+        filter: "*.wav;*.mp3"
+    }
+
+    Files {
+        filter: "*.mp4"
+    }
+
+    Files {
+        filter: "*.glsl;*.glslv;*.glslf;*.vsh;*.fsh;*.vert;*.frag"
+    }
+
+    Files {
+        filter: "*.qsb"
+    }
+
+    Files {
+        filter: "*.json"
+    }
+
+    Files {
+        filter: "*.mesh"
+        directory: "Generated"
+    }
+
+    Files {
+        filter: "*.qad"
+        directory: "Generated"
+    }
+
+    Environment {
+        QT_QUICK_CONTROLS_CONF: "qtquickcontrols2.conf"
+        QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT: "1"
+        QT_LOGGING_RULES: "qt.qml.connections=false"
+        QT_ENABLE_HIGHDPI_SCALING: "0"
+        /* Useful for debugging
+       QSG_VISUALIZE=batches
+       QSG_VISUALIZE=clip
+       QSG_VISUALIZE=changes
+       QSG_VISUALIZE=overdraw
+       */
+    }
+
+    qt6Project: true
+
+    /* List of plugin directories passed to QML runtime */
+    importPaths: [ "." ]
+
+    /* Required for deployment */
+    targetDirectory: "/opt/UntitledProject49"
+
+
+    qdsVersion: "4.7"
+
+    quickVersion: "6.8"
+
+    /* If any modules the project imports require widgets (e.g. QtCharts), widgetApp must be true */
+    widgetApp: true
+
+    /* args: Specifies command line arguments for qsb tool to generate shaders.
+       files: Specifies target files for qsb tool. If path is included, it must be relative to this file.
+              Wildcard '*' can be used in the file name part of the path.
+              e.g. files: [ "UntitledProject49Content/shaders/*.vert", "*.frag" ]  */
+    ShaderTool {
+        args: "-s --glsl \"100 es,120,150\" --hlsl 50 --msl 12"
+        files: [ "test_projectContent/shaders/*" ]
+    }
+
+    multilanguageSupport: true
+    supportedLanguages: ["en"]
+    primaryLanguage: "en"
+
+}
diff --git a/tests/data/test_project/test_project/Constants.qml b/tests/data/test_project/test_project/Constants.qml
new file mode 100644
index 0000000000000000000000000000000000000000..e134a568b8534f24a27ca3ddf8f936bc07002109
--- /dev/null
+++ b/tests/data/test_project/test_project/Constants.qml
@@ -0,0 +1,27 @@
+pragma Singleton
+import QtQuick
+import QtQuick.Studio.Application
+
+QtObject {
+    readonly property int width: 1920
+    readonly property int height: 1080
+
+    property string relativeFontDirectory: "fonts"
+
+    /* Edit this comment to add your custom font */
+    readonly property font font: Qt.font({
+                                             family: Qt.application.font.family,
+                                             pixelSize: Qt.application.font.pixelSize
+                                         })
+    readonly property font largeFont: Qt.font({
+                                                  family: Qt.application.font.family,
+                                                  pixelSize: Qt.application.font.pixelSize * 1.6
+                                              })
+
+    readonly property color backgroundColor: "#EAEAEA"
+
+
+    property StudioApplication application: StudioApplication {
+        fontPath: Qt.resolvedUrl("../UntitledProject49Content/" + relativeFontDirectory)
+    }
+}
diff --git a/tests/data/test_project/test_project/EventListModel.qml b/tests/data/test_project/test_project/EventListModel.qml
new file mode 100644
index 0000000000000000000000000000000000000000..00c70659823285878690ed15d1da57df13141d5a
--- /dev/null
+++ b/tests/data/test_project/test_project/EventListModel.qml
@@ -0,0 +1,12 @@
+import QtQuick
+
+ListModel {
+    id: eventListModel
+
+    ListElement {
+        eventId: "enterPressed"
+        eventDescription: "Emitted when pressing the enter button"
+        shortcut: "Return"
+        parameters: "Enter"
+    }
+}
diff --git a/tests/data/test_project/test_project/EventListSimulator.qml b/tests/data/test_project/test_project/EventListSimulator.qml
new file mode 100644
index 0000000000000000000000000000000000000000..d26ae6d50718a28b22720b417f0c7800d24f4aca
--- /dev/null
+++ b/tests/data/test_project/test_project/EventListSimulator.qml
@@ -0,0 +1,22 @@
+import QtQuick
+import QtQuick.Studio.EventSimulator
+import QtQuick.Studio.EventSystem
+
+QtObject {
+    id: simulator
+    property bool active: true
+
+    property Timer __timer: Timer {
+        id: timer
+        interval: 100
+        onTriggered: {
+            EventSimulator.show()
+        }
+    }
+
+    Component.onCompleted: {
+        EventSystem.init(Qt.resolvedUrl("EventListModel.qml"))
+        if (simulator.active)
+            timer.start()
+    }
+}
diff --git a/tests/data/test_project/test_project/designer/plugin.metainfo b/tests/data/test_project/test_project/designer/plugin.metainfo
new file mode 100644
index 0000000000000000000000000000000000000000..cef86901ea6ac8c39cf4adcb59c504ed1dc7ddf9
--- /dev/null
+++ b/tests/data/test_project/test_project/designer/plugin.metainfo
@@ -0,0 +1,13 @@
+MetaInfo {
+    Type {
+        name: "UntitledProject49.EventListSimulator"
+        icon: ":/qtquickplugin/images/item-icon16.png"
+
+        Hints {
+            visibleInNavigator: true
+            canBeDroppedInNavigator: true
+            canBeDroppedInFormEditor: false
+            canBeDroppedInView3D: false
+        }
+    }
+}
diff --git a/tests/data/test_project/test_project/qmldir b/tests/data/test_project/test_project/qmldir
new file mode 100644
index 0000000000000000000000000000000000000000..3fc49f80682d63bfc2487ea5e0fcc753c6fc0c1d
--- /dev/null
+++ b/tests/data/test_project/test_project/qmldir
@@ -0,0 +1,4 @@
+module UntitledProject49
+singleton Constants 1.0 Constants.qml
+EventListSimulator 1.0 EventListSimulator.qml
+EventListModel 1.0 EventListModel.qml
diff --git a/tests/data/test_project/test_projectContent/App.qml b/tests/data/test_project/test_projectContent/App.qml
new file mode 100644
index 0000000000000000000000000000000000000000..cadefcd82742d20df0e68beeb0fbe9721307b73e
--- /dev/null
+++ b/tests/data/test_project/test_projectContent/App.qml
@@ -0,0 +1,16 @@
+import QtQuick
+import test_project
+
+Window {
+    width: mainScreen.width
+    height: mainScreen.height
+
+    visible: true
+    title: "test_project"
+
+    Screen01 {
+        id: mainScreen
+    }
+
+}
+
diff --git a/tests/data/test_project/test_projectContent/Screen01.ui.qml b/tests/data/test_project/test_projectContent/Screen01.ui.qml
new file mode 100644
index 0000000000000000000000000000000000000000..ac24a0f9b4014761409eccd6e2850aa41be17ecd
--- /dev/null
+++ b/tests/data/test_project/test_projectContent/Screen01.ui.qml
@@ -0,0 +1,254 @@
+
+
+/*
+This is a UI file (.ui.qml) that is intended to be edited in Qt Design Studio only.
+It is supposed to be strictly declarative and only uses a subset of QML. If you edit
+this file manually, you might introduce QML code that is not supported by Qt Design Studio.
+Check out https://doc.qt.io/qtcreator/creator-quick-ui-forms.html for details on .ui.qml files.
+*/
+import QtQuick
+import QtQuick.Controls
+import test_project
+import QtQuick.Studio.DesignEffects
+import QtQuick.Studio.Components
+import QtQuick.Studio.Effects
+import QtQuick.Studio.LogicHelper
+import QtQuick.Studio.MultiText
+import QtCharts
+import QtGraphs
+import QtQuick.Layouts
+import QtQuick3D
+import QtQuick.Controls.FluentWinUI3
+import QtQuick.Effects
+import QtQuick.Studio.Utils
+import QtQuick.Studio.Application
+import QtQuick.Timeline.BlendTrees
+import QtQuick.VectorImage
+import QtQuick.Window
+import QtQuick3D.AssetUtils
+import QtQuick3D.Effects
+import QtQuick3D.Helpers
+import QtQuick3D.Particles3D
+import QtQuick3D.Physics
+import QtQuick.Timeline
+import QtNetwork
+import QtQuick3D.Physics.Helpers
+import QtMultimedia
+import QtQuick3D.SpatialAudio
+import QtQuick3D.Xr
+import QtQuickUltralite.Extras
+import QtQuickUltralite.Studio.Components
+import QtQuickUltralite.Profiling
+import QtQuickUltralite.Layers
+import FlowView
+import test
+
+Rectangle {
+    id: rectangle
+    width: Constants.width
+    height: Constants.height
+
+    color: Constants.backgroundColor
+
+    Button {
+        id: button
+        text: qsTr("Press me")
+        anchors.verticalCenter: parent.verticalCenter
+        checkable: true
+        anchors.horizontalCenter: parent.horizontalCenter
+
+        Connections {
+            target: button
+            onClicked: animation.start()
+        }
+
+        DesignEffect {
+            backgroundLayer: button
+            backgroundBlurRadius: 5
+            layerBlurRadius: 2
+            effects: [
+                DesignDropShadow {},
+                DesignDropShadow {}
+            ]
+        }
+    }
+
+    Text {
+        id: label
+        text: qsTr("Hello UntitledProject49")
+        anchors.top: button.bottom
+        font.family: Constants.font.family
+        anchors.topMargin: 45
+        anchors.horizontalCenter: parent.horizontalCenter
+
+        SequentialAnimation {
+            id: animation
+
+            ColorAnimation {
+                id: colorAnimation1
+                target: rectangle
+                property: "color"
+                to: "#2294c6"
+                from: Constants.backgroundColor
+            }
+
+            ColorAnimation {
+                id: colorAnimation2
+                target: rectangle
+                property: "color"
+                to: Constants.backgroundColor
+                from: "#2294c6"
+            }
+        }
+    }
+
+    Button {
+        id: button1
+        x: 930
+        y: 466
+        text: qsTr("Button")
+    }
+
+    CheckBox {
+        id: checkBox
+        x: 918
+        y: 417
+        text: qsTr("Check Box")
+    }
+
+    ComboBox {
+        id: comboBox
+        x: 900
+        y: 563
+    }
+
+    RadioButton {
+        id: radioButton
+        x: 897
+        y: 632
+        text: qsTr("Radio Button")
+    }
+
+    RadioDelegate {
+        id: radioDelegate
+        x: 881
+        y: 677
+        text: qsTr("Radio Delegate")
+    }
+
+    RoundButton {
+        id: roundButton
+        x: 864
+        y: 490
+        text: "+"
+    }
+
+    Slider {
+        id: slider
+        x: 859
+        y: 375
+        value: 0.5
+    }
+
+    RangeSlider {
+        id: rangeSlider
+        x: 859
+        y: 333
+        second.value: 0.75
+        first.value: 0.25
+    }
+
+    Switch {
+        id: _switch
+        x: 845
+        y: 438
+        text: qsTr("Switch")
+    }
+
+    Button {
+        id: button2
+        x: 1090
+        y: 473
+        text: qsTr("Button")
+    }
+
+    ArcItem {
+        id: arc
+        x: 1090
+        y: 333
+        fillColor: "#00000000"
+    }
+
+    EllipseItem {
+        id: ellipse
+        x: 628
+        y: 333
+    }
+
+    BorderItem {
+        id: _border
+        x: 628
+        y: 508
+        adjustBorderRadius: true
+    }
+
+    FlipableItem {
+        id: flipable
+        x: 1066
+        y: 532
+    }
+
+    TriangleItem {
+        id: triangle
+        x: 669
+        y: 677
+    }
+
+    Item {
+        id: __materialLibrary__
+    }
+
+    ChartView {
+        id: bar
+        x: 1257
+        y: 340
+        width: 300
+        height: 300
+        BarSeries {
+            name: "BarSeries"
+            BarSet {
+                values: [2, 2, 3]
+                label: "Set1"
+            }
+
+            BarSet {
+                values: [5, 1, 2]
+                label: "Set2"
+            }
+
+            BarSet {
+                values: [3, 5, 8]
+                label: "Set3"
+            }
+        }
+    }
+
+    DesignEffect {
+        visible: true
+        effects: [
+            DesignInnerShadow {},
+            DesignDropShadow {}
+        ]
+    }
+    states: [
+        State {
+            name: "clicked"
+            when: button.checked
+
+            PropertyChanges {
+                target: label
+                text: qsTr("Button Checked")
+            }
+        }
+    ]
+}
diff --git a/tests/data/test_project/test_projectContent/fonts/fonts.txt b/tests/data/test_project/test_projectContent/fonts/fonts.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ab96122067450783de407d6200171a97ab3849be
--- /dev/null
+++ b/tests/data/test_project/test_projectContent/fonts/fonts.txt
@@ -0,0 +1 @@
+Fonts in this folder are loaded automatically.
diff --git a/tests/data/test_project/test_projectContent/images/images.txt b/tests/data/test_project/test_projectContent/images/images.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f8b999661ea6fb6c57246fba02c15a578bd64cf5
--- /dev/null
+++ b/tests/data/test_project/test_projectContent/images/images.txt
@@ -0,0 +1 @@
+Default folder for image assets.
diff --git a/tests/data/test_project/test_project_fail.qmlrc b/tests/data/test_project/test_project_fail.qmlrc
new file mode 100644
index 0000000000000000000000000000000000000000..ff815586b54fb2e038e38fb1965bac390af59b65
Binary files /dev/null and b/tests/data/test_project/test_project_fail.qmlrc differ
diff --git a/tests/data/test_project/test_project_success.qmlrc b/tests/data/test_project/test_project_success.qmlrc
new file mode 100644
index 0000000000000000000000000000000000000000..1b124cb3c43f3d6c054af130f5a5286e8da78273
Binary files /dev/null and b/tests/data/test_project/test_project_success.qmlrc differ
diff --git a/tests/main.cpp b/tests/main.cpp
index 7076f505a61f06f47306f4445c6e3ec18f270a90..dad15107f916ea9193b0e29ede6c19ce1dbbc84e 100644
--- a/tests/main.cpp
+++ b/tests/main.cpp
@@ -1,17 +1,20 @@
 // Copyright (C) 2024 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
 
-#include <QGuiApplication>
+#include <QApplication>
 #include <QTemporaryFile>
 #include <QTest>
 
+#include "tst_dsmanager.h"
+#include "tst_projectmanager.h"
 #include "tst_settings.h"
 
-#define DEBUG_LINE qDebug() << "Debugline:" << __LINE__
+#define DEBUG_LINE qDebug() << "Debug (" << __LINE__ << "):"
 
 int main(int argc, char *argv[])
 {
-    QGuiApplication app(argc, argv);
+    qputenv("QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT", "1");
+    QApplication app(argc, argv);
 
     QStringList initialArgs = app.arguments();
     DEBUG_LINE << "Arguments:" << initialArgs;
@@ -33,14 +36,14 @@ int main(int argc, char *argv[])
     }
 
     if (outputFileName.isEmpty()) {
-        qDebug() << "Output file name not provided. Using the default name: output.junitxml";
+        DEBUG_LINE << "Output file name not provided. Using the default name: output.junitxml";
         outputFileName = "output.junitxml";
     }
 
     QFile outputFile{outputFileName};
-    if (!outputFile.open(QIODevice::Append)) {
+    if (!outputFile.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
         DEBUG_LINE << "Failed to open the output file";
-        return 1;
+        return -1;
     }
 
     int status = 0;
@@ -48,8 +51,11 @@ int main(int argc, char *argv[])
         // temporary file to store the individual test results.
         // we'll append the results to the output file.
         QTemporaryFile file;
-        file.open();
-        DEBUG_LINE << "Temporary file:" << file.fileName();
+        if (!file.open()) {
+            DEBUG_LINE << "Failed to open the temporary file";
+            status = -1;
+            return;
+        }
 
         QStringList args{"-o", QString(file.fileName()).append(",junitxml")};
         args.prepend(appName);
@@ -60,15 +66,24 @@ int main(int argc, char *argv[])
 
         // append the test results to the output file
         file.readLine(); // skip the first line because it's the xml header
-        outputFile.write(file.readAll());
+        const int rV = outputFile.write(file.readAll());
+        if (rV == -1) {
+            DEBUG_LINE << "Failed to write the test results to the output file";
+            status = -1;
+            return;
+        }
     };
 
     outputFile.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
     outputFile.write("<testsuites>\n");
 
     runTest(new TestSettings);
+    runTest(new TestDesignStudioManager);
+    runTest(new TestProjectManager);
 
     outputFile.write("</testsuites>\n");
+    outputFile.flush();
+    outputFile.close();
 
     DEBUG_LINE << "Test suite finished with status:" << status;
     return status;
diff --git a/tests/mock_ds.cpp b/tests/mock_ds.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..14955957688e4a164fbbb2bac1cd89b1efccea5e
--- /dev/null
+++ b/tests/mock_ds.cpp
@@ -0,0 +1,129 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "mock_ds.h"
+
+#include <QJsonParseError>
+#include <QJsonObject>
+#include <QJsonDocument>
+
+namespace PackageToDevice {
+using namespace Qt::Literals;
+constexpr auto designStudioReady = "designStudioReady"_L1;
+constexpr auto projectData = "projectData"_L1;
+constexpr auto stopRunningProject = "stopRunningProject"_L1;
+}; // namespace PackageToDevice
+
+namespace PackageFromDevice {
+using namespace Qt::Literals;
+constexpr auto deviceInfo = "deviceInfo"_L1;
+constexpr auto projectReceivingProgress = "projectReceivingProgress"_L1;
+constexpr auto projectStarting = "projectStarting"_L1;
+constexpr auto projectStarted = "projectRunning"_L1;
+constexpr auto projectStopped = "projectStopped"_L1;
+constexpr auto projectLogs = "projectLogs"_L1;
+}; // namespace PackageFromDevice
+
+MockDS::MockDS(const QString dsID,
+               const QString &serverAddr,
+               const QString &serverName,
+               QObject *parent)
+    : QObject(parent)
+    , m_dsID(dsID)
+    , m_serverAddr(serverAddr)
+{
+      m_socket.reset(new QWebSocket(serverName, QWebSocketProtocol::VersionLatest, this));
+      QObject::connect(m_socket.data(),
+                       &QWebSocket::textMessageReceived,
+                       this,
+                       &MockDS::textMessageReceived);
+      connect();
+}
+
+MockDS::~MockDS()
+{
+    m_socket->close();
+}
+
+void MockDS::connect()
+{
+    m_socket->open(QUrl(m_serverAddr));
+}
+
+void MockDS::disconnect()
+{
+    m_socket->close();
+    m_socket->abort();
+}
+
+bool MockDS::isConnected() const
+{
+    return m_socket->isValid();
+}
+
+bool MockDS::sendTextMessage(const QLatin1String &dataType, const QJsonValue &data)
+{
+    if (!isConnected())
+        return false;
+
+    QJsonObject message;
+    message["dataType"] = dataType;
+    message["data"] = data;
+    const QString jsonMessage = QString::fromLatin1(
+        QJsonDocument(message).toJson(QJsonDocument::Compact));
+    m_socket->sendTextMessage(jsonMessage);
+
+    return true;
+}
+
+bool MockDS::sendDesignStudioReady()
+{
+    QJsonObject data;
+    data["designStudioID"] = m_dsID;
+    data["commVersion"] = 1;
+    return sendTextMessage(PackageToDevice::designStudioReady, data);
+}
+
+bool MockDS::sendProjectData(const QByteArray &data, const QString &qtVersion)
+{
+    QJsonObject projectInfo;
+    projectInfo["projectSize"] = data.size();
+    projectInfo["qtVersion"] = qtVersion;
+
+    const bool rV = sendTextMessage(PackageToDevice::projectData, projectInfo);
+    if (!rV)
+        return false;
+
+    const int dataSent = m_socket->sendBinaryMessage(data);
+    return dataSent == data.size();
+}
+
+bool MockDS::sendProjectStopped()
+{
+    return sendTextMessage(PackageToDevice::stopRunningProject, {});
+}
+
+void MockDS::textMessageReceived(const QString &message){
+    qDebug () << "Message received: " << message;
+    QJsonParseError jsonError;
+    const QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toLatin1(), &jsonError);
+
+    const QJsonObject jsonObj = jsonDoc.object();
+    const QString dataType = jsonObj.value("dataType").toString();
+    if (dataType == PackageFromDevice::deviceInfo) {
+        QJsonObject deviceInfo = jsonObj.value("data").toObject();
+        emit deviceInfoReady();
+    } else if (dataType == PackageFromDevice::projectStarted) {
+        emit projectStarted();
+    } else if (dataType == PackageFromDevice::projectStopped) {
+        emit projectStopped();
+    } else if (dataType == PackageFromDevice::projectLogs) {
+        const QString logs = jsonObj.value("data").toString();
+        emit projectLogsReceived(logs);
+    } else if (dataType == PackageFromDevice::projectStarting) {
+        emit projectStarting();
+    } else if (dataType == PackageFromDevice::projectReceivingProgress) {
+        const int progress = jsonObj.value("data").toInt();
+        emit projectSendingProgress(progress);
+    }
+}
diff --git a/tests/mock_ds.h b/tests/mock_ds.h
new file mode 100644
index 0000000000000000000000000000000000000000..1214accc59019b2f276f5f984febfb20fe228bfb
--- /dev/null
+++ b/tests/mock_ds.h
@@ -0,0 +1,43 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#pragma once
+
+#include <QWebSocket>
+
+/**
+ * @brief This is a mock class to simulate the Design Studio connection.
+ * This one acts like a client and the DesignStudioManager acts like a server.
+ * This can be used to test the data coming from the Design Studio or the vice versa.
+ */
+class MockDS : public QObject
+{
+    Q_OBJECT
+public:
+    MockDS(const QString dsID, const QString &serverAddr, const QString &serverName, QObject *parent = nullptr);
+    ~MockDS();
+
+    void connect();
+    void disconnect();
+    bool isConnected() const;
+    bool sendDesignStudioReady();
+    bool sendProjectData(const QByteArray &data, const QString &qtVersion);
+    bool sendProjectStopped();
+
+private:
+    QScopedPointer<QWebSocket> m_socket;
+    const QString m_dsID;
+    const QString m_serverAddr;
+
+private:
+    void textMessageReceived(const QString &message);
+    bool sendTextMessage(const QLatin1String &dataType, const QJsonValue &data);
+
+signals:
+    void projectStarted();
+    void projectStarting();
+    void projectStopped();
+    void projectLogsReceived(const QString &logs);
+    void deviceInfoReady();
+    void projectSendingProgress(const int percentage);
+};
diff --git a/tests/tst_dsmanager.cpp b/tests/tst_dsmanager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d84669a4a839e6c67fa505f48f4f0003c8befa3f
--- /dev/null
+++ b/tests/tst_dsmanager.cpp
@@ -0,0 +1,221 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+#include "tst_dsmanager.h"
+
+#include <QTest>
+#include <QSignalSpy>
+
+#define DEVICE_ID "1234567890"
+#define DS_ID "0987654321"
+#define CONNECTION_TIMEOUT 1000
+
+TestObjects::TestObjects(const quint16 port, QObject *parent)
+    : QObject(parent)
+{
+    m_dsManager.reset(new DesignStudioManager(DEVICE_ID, port));
+    m_dsManager->init();
+
+    m_mockDS.reset(new MockDS(DS_ID, QString("ws://localhost:%1").arg(port), "DesignStudio"));
+
+    const bool rV = QTest::qWaitFor([&]() { return m_mockDS->isConnected(); }, CONNECTION_TIMEOUT);
+    QVERIFY(rV == true);
+
+    QSignalSpy spy(m_mockDS.data(), &MockDS::deviceInfoReady);
+    QVERIFY(m_mockDS->sendDesignStudioReady() == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+
+    m_mockDS2.reset(new MockDS(DS_ID "2", QString("ws://localhost:%1").arg(port), "DesignStudio"));
+    const bool rV2 = QTest::qWaitFor([&]() { return m_mockDS2->isConnected(); }, CONNECTION_TIMEOUT);
+    QVERIFY(rV2 == true);
+
+    QSignalSpy spy2(m_mockDS2.data(), &MockDS::deviceInfoReady);
+    QVERIFY(m_mockDS2->sendDesignStudioReady() == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+DesignStudioManager *TestObjects::designStudioManager()
+{
+    return m_dsManager.data();
+}
+
+MockDS *TestObjects::mockDS()
+{
+    return m_mockDS.data();
+}
+
+MockDS *TestObjects::mockDS2()
+{
+    return m_mockDS2.data();
+}
+
+void TestDesignStudioManager::initTestCase()
+{
+    QFile file(":/data/test_project/test_project_success.qmlrc");
+    qDebug() << "Reading file:" << file.fileName();
+    QVERIFY(file.open(QIODevice::ReadOnly));
+
+    m_projectData = file.readAll();
+    QVERIFY(m_projectData.size() > 0);
+
+    m_dsManager.reset(new DesignStudioManager(DEVICE_ID));
+    m_dsManager->init();
+
+    const QString serverName = m_dsManager->webSocketServerName();
+    const int serverPort = m_dsManager->webSocketServerPort();
+    // Important note:
+    // If the following 2 parameters change by a design decision,
+    // they have to be updated on Qt Design Studio side as well.
+    QVERIFY2(serverName == "DesignStudio",
+             "Default server name is not correct. Update the test and Design Studio side.");
+    QVERIFY2(serverPort == 40000,
+             "Default server port is not correct. Update the test and Design Studio side.");
+
+    const QString serverAddr = QString("ws://localhost:%1").arg(serverPort);
+
+    m_mockDS.reset(new MockDS(DS_ID, serverAddr, serverName));
+    const bool rV = QTest::qWaitFor([&]() { return m_mockDS->isConnected(); }, CONNECTION_TIMEOUT);
+    QVERIFY(rV == true);
+
+    QSignalSpy spy(m_mockDS.data(), &MockDS::deviceInfoReady);
+    QVERIFY(m_mockDS->sendDesignStudioReady() == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testSendProjectStarting()
+{
+    QSignalSpy spy(m_mockDS.data(), &MockDS::projectStarting);
+    m_dsManager->sendProjectStarting(DS_ID);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testSendProjectStarted()
+{
+    QSignalSpy spy(m_mockDS.data(), &MockDS::projectStarted);
+    m_dsManager->sendProjectStarted(DS_ID);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testSendProjectStopped()
+{
+    QSignalSpy spy(m_mockDS.data(), &MockDS::projectStopped);
+    m_dsManager->sendProjectStopped(DS_ID);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testSendProjectLogs()
+{
+    QSignalSpy spy(m_mockDS.data(), &MockDS::projectLogsReceived);
+    m_dsManager->sendProjectLogs(DS_ID, "Test logs");
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testProjectReceivingProgress()
+{
+    TestObjects testObjects(40001);
+    const QString qtVersion = "6.8.0";
+
+    QSignalSpy spy(testObjects.mockDS(), &MockDS::projectSendingProgress);
+    QVERIFY(testObjects.mockDS()->sendProjectData(m_projectData, qtVersion) == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testGetDesignStudioIp()
+{
+    TestObjects testObjects(40001);
+    const QString ipAddr = testObjects.designStudioManager()->getDesignStudioIp(DS_ID);
+    const QString failMessage = QString("Design Studio IP is not correct. Expected: %1, Got: %2")
+                                    .arg("::1 or ::ffff:127.0.0.1")
+                                    .arg(ipAddr);
+
+    if (ipAddr != "::1" && ipAddr != "::ffff:127.0.0.1")
+        QFAIL(qPrintable(failMessage));
+
+    QVERIFY(testObjects.designStudioManager()->getDesignStudioIp("non-existing-id").isEmpty());
+}
+
+void TestDesignStudioManager::testProjectVersionMismatch()
+{
+    TestObjects testObjects(40002);
+    const QString qtVersion = "5.0.0";
+
+    QSignalSpy spy(testObjects.designStudioManager(), &DesignStudioManager::projectVersionMismatch);
+    QVERIFY(testObjects.mockDS()->sendProjectData(m_projectData, qtVersion) == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testProjectIncoming()
+{
+    TestObjects testObjects(40003);
+    const QString qtVersion = "6.8.0";
+
+    QSignalSpy spy(testObjects.designStudioManager(), &DesignStudioManager::projectIncoming);
+    QVERIFY(testObjects.mockDS()->sendProjectData(m_projectData, qtVersion) == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testProjectIncomingProgress()
+{
+    TestObjects testObjects(40004);
+
+    const QString qtVersion = "6.8.0";
+    QByteArray bigProjectdata;
+    bigProjectdata.fill('a', 1024 * 1024 * 10);
+
+    QSignalSpy spy(testObjects.designStudioManager(), &DesignStudioManager::projectIncomingProgress);
+    QVERIFY(testObjects.mockDS()->sendProjectData(bigProjectdata, qtVersion) == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testProjectReceived()
+{
+    TestObjects testObjects(40005);
+    const QString qtVersion = "6.8.0";
+
+    QSignalSpy spy(testObjects.designStudioManager(), &DesignStudioManager::projectReceived);
+    QVERIFY(testObjects.mockDS()->sendProjectData(m_projectData, qtVersion) == true);
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testDesignStudioConnected()
+{
+    TestObjects testObjects(40006);
+
+    QSignalSpy spy(testObjects.designStudioManager(), &DesignStudioManager::designStudioConnected);
+    testObjects.mockDS()->disconnect();
+    testObjects.mockDS()->connect();
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testDesignStudioDisconnected()
+{
+    TestObjects testObjects(40007);
+
+    QSignalSpy spy(testObjects.designStudioManager(),
+                   &DesignStudioManager::designStudioDisconnected);
+    testObjects.mockDS()->disconnect();
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
+
+void TestDesignStudioManager::testAllDesignStudiosDisconnected()
+{
+    TestObjects testObjects(40008);
+
+    QSignalSpy spy(testObjects.designStudioManager(),
+                   &DesignStudioManager::designStudioDisconnected);
+    QSignalSpy spy2(testObjects.designStudioManager(),
+                    &DesignStudioManager::allDesignStudiosDisconnected);
+    testObjects.mockDS()->disconnect();
+    testObjects.mockDS2()->disconnect();
+    spy.wait(1000);
+    QCOMPARE(spy.count(), 2);
+    QCOMPARE(spy2.count(), 1);
+}
+
+void TestDesignStudioManager::testProjectStopRequested()
+{
+    TestObjects testObjects(40009);
+
+    QSignalSpy spy(testObjects.designStudioManager(), &DesignStudioManager::projectStopRequested);
+    testObjects.mockDS()->sendProjectStopped();
+    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 1000);
+}
diff --git a/tests/tst_dsmanager.h b/tests/tst_dsmanager.h
new file mode 100644
index 0000000000000000000000000000000000000000..e573518f0e3ecaa15aeaf49a52dabc68199a2f5b
--- /dev/null
+++ b/tests/tst_dsmanager.h
@@ -0,0 +1,54 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#pragma once
+
+#include <QObject>
+
+#include "backend/dsconnector/dsmanager.h"
+#include "mock_ds.h"
+
+class TestObjects : public QObject
+{
+    Q_OBJECT
+public:
+    TestObjects(const quint16 port = 40000, QObject *parent = nullptr);
+    DesignStudioManager *designStudioManager();
+    MockDS *mockDS();
+    MockDS *mockDS2();
+
+private:
+    QScopedPointer<DesignStudioManager> m_dsManager;
+    QScopedPointer<MockDS> m_mockDS;
+    QScopedPointer<MockDS> m_mockDS2;
+};
+
+class TestDesignStudioManager : public QObject
+{
+    Q_OBJECT
+private slots:
+    void initTestCase();
+
+    // to DesignStudio
+    void testSendProjectStarted();
+    void testSendProjectStarting();
+    void testSendProjectStopped();
+    void testSendProjectLogs();
+    void testProjectReceivingProgress();
+    void testGetDesignStudioIp();
+
+    // to Android
+    void testProjectVersionMismatch();
+    void testProjectIncoming();
+    void testProjectIncomingProgress();
+    void testProjectReceived();
+    void testDesignStudioConnected();
+    void testDesignStudioDisconnected();
+    void testAllDesignStudiosDisconnected();
+    void testProjectStopRequested();
+
+private:
+    QScopedPointer<DesignStudioManager> m_dsManager;
+    QScopedPointer<MockDS> m_mockDS;
+    QByteArray m_projectData;
+};
diff --git a/tests/tst_projectmanager.cpp b/tests/tst_projectmanager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1be6c918c8a96810aa469cd5b1931f98ec2049e7
--- /dev/null
+++ b/tests/tst_projectmanager.cpp
@@ -0,0 +1,211 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "tst_projectmanager.h"
+
+#include <QQuickItem>
+#include <QSignalSpy>
+#include <QTest>
+
+void TestProjectManager::initTestCase()
+{
+    QFile file(":/data/test_project/test_project_success.qmlrc");
+    QVERIFY(file.open(QIODevice::ReadOnly));
+
+    m_projectThatCanRun = file.readAll();
+    QVERIFY(m_projectThatCanRun.size() > 0);
+
+    file.setFileName(":/data/test_project/test_project_fail.qmlrc");
+    QVERIFY(file.open(QIODevice::ReadOnly));
+
+    m_projectThatCannotRun = file.readAll();
+    QVERIFY(m_projectThatCannotRun.size() > 0);
+}
+
+void TestProjectManager::testRunProject()
+{
+    ProjectManager projectManager;
+    bool result = projectManager.runProject(m_projectThatCanRun, true, "sessionId");
+
+    QVERIFY(projectManager.sessionId() == "sessionId");
+    QVERIFY(result);
+
+    result = projectManager.runProject(m_projectThatCannotRun, true, "sessionId");
+    QVERIFY(!result);
+    QVERIFY(projectManager.sessionId().isEmpty());
+}
+
+void TestProjectManager::testStopProject()
+{
+    ProjectManager projectManager;
+    const bool result = projectManager.runProject(m_projectThatCanRun, true, "sessionId");
+    QVERIFY(result);
+
+    projectManager.stopProject();
+    QVERIFY(projectManager.sessionId().isEmpty());
+    QVERIFY(projectManager.m_quickWindow.isNull());
+    QVERIFY(projectManager.m_qmlEngine.isNull());
+    QVERIFY(projectManager.m_qmlComponent.isNull());
+}
+
+void TestProjectManager::testRegisterResource()
+{
+    ProjectManager projectManager;
+    const QString resourcePath = ":/data/test_project_register";
+    bool result = projectManager.registerResource(m_projectThatCanRun, resourcePath);
+    QVERIFY(result);
+
+    QFile file(resourcePath + "/test_project.qmlproject");
+    QVERIFY(file.open(QIODevice::ReadOnly));
+    QByteArray data = file.readAll();
+    QVERIFY(data.size() > 0);
+    QVERIFY(data.contains("import QmlProject"));
+    QVERIFY(data.contains("mainFile: \"test_projectContent/App.qml\""));
+
+    file.close();
+    file.setFileName(resourcePath + "/qtquickcontrols2.conf");
+    QVERIFY(file.open(QIODevice::ReadOnly));
+    data = file.readAll();
+    QVERIFY(data.size() > 0);
+    QVERIFY(data.contains("Style=Basic"));
+}
+
+void TestProjectManager::testUnregisterResource()
+{
+    ProjectManager projectManager;
+    const QString resourcePath = ":/data/test_project_unregister";
+    bool result = projectManager.registerResource(m_projectThatCanRun, resourcePath);
+    QVERIFY(result);
+
+    result = projectManager.unregisterResource(m_projectThatCanRun, resourcePath);
+    QVERIFY(result);
+
+    const QFile file(resourcePath + "/test_project.qmlproject");
+    QVERIFY(!file.exists());
+}
+
+void TestProjectManager::testCopyResourceToFs()
+{
+    ProjectManager projectManager;
+    const QString resourcePath = ":/data/test_project_copy";
+    bool result = projectManager.registerResource(m_projectThatCanRun, resourcePath);
+    QVERIFY(result);
+
+    const QString destinationPath = QDir::tempPath() + "/test_project_copy";
+    result = projectManager.copyResourceToFs(resourcePath, destinationPath);
+    QVERIFY(result);
+
+    const QStringList sourceFiles = QDir(resourcePath).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+    const QStringList destFiles = QDir(destinationPath).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+
+    QVERIFY(sourceFiles.size() == destFiles.size());
+    QCOMPARE(sourceFiles, destFiles);
+}
+
+void TestProjectManager::testUnpackProject()
+{
+    ProjectManager projectManager;
+    const QString projectPath = projectManager.unpackProject(m_projectThatCanRun);
+    QVERIFY(!projectPath.isEmpty());
+    QVERIFY(QFile::exists(projectPath + "/test_project.qmlproject"));
+    QVERIFY(QFile::exists(projectPath + "/qtquickcontrols2.conf"));
+}
+
+void TestProjectManager::testRunProjectInternal()
+{
+    ProjectManager projectManager;
+
+    const bool result = projectManager.runProjectInternal(m_projectThatCanRun);
+
+    QVERIFY(result);
+    QVERIFY(projectManager.m_qmlEngine);
+    QVERIFY(projectManager.m_qmlComponent);
+    QVERIFY(projectManager.m_quickWindow);
+    QVERIFY(projectManager.m_quickWindow->isVisible() == false);
+}
+
+void TestProjectManager::testFindFile()
+{
+    ProjectManager projectManager;
+    const QString projectPath = projectManager.unpackProject(m_projectThatCanRun);
+    QVERIFY(!projectPath.isEmpty());
+
+    const QString qmlProjectFile = projectManager.findFile(projectPath, "*.qmlproject", false);
+    QVERIFY(!qmlProjectFile.isEmpty());
+    QVERIFY(qmlProjectFile.contains("test_project.qmlproject"));
+}
+
+void TestProjectManager::testReadQmlProjectFile()
+{
+    ProjectManager projectManager;
+    const QString projectPath = projectManager.unpackProject(m_projectThatCanRun);
+    QVERIFY(!projectPath.isEmpty());
+
+    const QString qmlProjectFile = projectManager.findFile(projectPath, "*.qmlproject", false);
+    QVERIFY(!qmlProjectFile.isEmpty());
+
+    const QString qmlProjectFileContent = projectManager.readQmlProjectFile(qmlProjectFile);
+    QVERIFY(!qmlProjectFileContent.isEmpty());
+    QVERIFY(qmlProjectFileContent.contains("mainFile: \"test_projectContent/App.qml\""));
+}
+
+void TestProjectManager::testGetMainQmlFile()
+{
+    ProjectManager projectManager;
+    const QString projectPath = projectManager.unpackProject(m_projectThatCanRun);
+    QVERIFY(!projectPath.isEmpty());
+
+    const QString qmlProjectFile = projectManager.findFile(projectPath, "*.qmlproject", false);
+    QVERIFY(!qmlProjectFile.isEmpty());
+
+    const QString qmlProjectFileContent = projectManager.readQmlProjectFile(qmlProjectFile);
+    QVERIFY(!qmlProjectFileContent.isEmpty());
+
+    const QString mainQmlFile = projectManager.getMainQmlFile(projectPath, qmlProjectFileContent);
+    QVERIFY(!mainQmlFile.isEmpty());
+    QCOMPARE(mainQmlFile, projectPath + "/test_projectContent/App.qml");
+}
+
+void TestProjectManager::testGetImportPaths()
+{
+    ProjectManager projectManager;
+    const QString projectPath = projectManager.unpackProject(m_projectThatCanRun);
+    QVERIFY(!projectPath.isEmpty());
+
+    const QString qmlProjectFile = projectManager.findFile(projectPath, "*.qmlproject", false);
+    QVERIFY(!qmlProjectFile.isEmpty());
+
+    const QString qmlProjectFileContent = projectManager.readQmlProjectFile(qmlProjectFile);
+    QVERIFY(!qmlProjectFileContent.isEmpty());
+
+    const QStringList importPaths = projectManager.getImportPaths(projectPath, qmlProjectFileContent);
+    QVERIFY(importPaths.size() == 1);
+    QCOMPARE(importPaths.at(0), projectPath + "/.");
+}
+
+void TestProjectManager::testIsQt6Project()
+{
+    ProjectManager projectManager;
+    const QString projectPath = projectManager.unpackProject(m_projectThatCanRun);
+    QVERIFY(!projectPath.isEmpty());
+
+    const QString qmlProjectFile = projectManager.findFile(projectPath, "*.qmlproject", false);
+    QVERIFY(!qmlProjectFile.isEmpty());
+
+    const QString qmlProjectFileContent = projectManager.readQmlProjectFile(qmlProjectFile);
+    QVERIFY(!qmlProjectFileContent.isEmpty());
+
+    const bool result = projectManager.isQt6Project(qmlProjectFileContent);
+    QVERIFY(result);
+}
+
+void TestProjectManager::testShowAppWindow()
+{
+    ProjectManager projectManager;
+
+    projectManager.runProject(m_projectThatCanRun, true, "sessionId");
+    QVERIFY(projectManager.m_quickWindow->isVisible() == false);
+
+    projectManager.showAppWindow();
+    QVERIFY(projectManager.m_quickWindow->isVisible());
+}
diff --git a/tests/tst_projectmanager.h b/tests/tst_projectmanager.h
new file mode 100644
index 0000000000000000000000000000000000000000..a9c5b603ccfd8eeb06b8e8f202ec21ba1ba274dc
--- /dev/null
+++ b/tests/tst_projectmanager.h
@@ -0,0 +1,40 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#pragma once
+
+#include <QObject>
+
+#include "backend/projectmanager.h"
+
+class TestProjectManager : public QObject
+{
+    Q_OBJECT
+private slots:
+    void initTestCase();
+
+    // public methods
+    void testRunProject();
+    void testStopProject();
+
+    // protected methods - resource management
+    void testRegisterResource();
+    void testUnregisterResource();
+    void testCopyResourceToFs();
+    void testUnpackProject();
+
+    // protected methods - project management
+    void testRunProjectInternal();
+    void testFindFile();
+    void testReadQmlProjectFile();
+    void testGetMainQmlFile();
+    void testGetImportPaths();
+    void testIsQt6Project();
+
+    // protected methods - window management
+    void testShowAppWindow();
+
+private:
+    QByteArray m_projectThatCanRun;
+    QByteArray m_projectThatCannotRun;
+};
diff --git a/tests/tst_settings.cpp b/tests/tst_settings.cpp
index a8922db75f2fa151bc0ebf1f2c22d21a9cbc91ba..f860d33ad0bc335e3df55be7c594f455d3ceeb3f 100644
--- a/tests/tst_settings.cpp
+++ b/tests/tst_settings.cpp
@@ -5,22 +5,31 @@
 
 #include <QTest>
 
-#include "backend/settings.h"
+void TestSettings::initTestCase()
+{
+    QVERIFY(m_settings.clearSettings());
+}
 
 void TestSettings::testAutoScaleProject()
 {
-    Settings settings;
-    QVERIFY(settings.autoScaleProject());
+    QVERIFY(m_settings.autoScaleProject());
 
-    settings.setAutoScaleProject(false);
-    QVERIFY(!settings.autoScaleProject());
+    m_settings.setAutoScaleProject(false);
+    QVERIFY(!m_settings.autoScaleProject());
 }
 
 void TestSettings::testDeviceUuid()
 {
-    Settings settings;
-    QVERIFY(settings.deviceUuid().isEmpty());
+    QVERIFY(m_settings.deviceUuid().isEmpty());
+
+    m_settings.setDeviceUuid("1234567890");
+    QCOMPARE(m_settings.deviceUuid(), QString("1234567890"));
+}
+
+void TestSettings::testKeepScreenOn()
+{
+    QVERIFY(!m_settings.keepScreenOn());
 
-    settings.setDeviceUuid("1234567890");
-    QCOMPARE(settings.deviceUuid(), QString("1234567890"));
+    m_settings.setKeepScreenOn(true);
+    QVERIFY(m_settings.keepScreenOn());
 }
diff --git a/tests/tst_settings.h b/tests/tst_settings.h
index 6c70c64d27b737a0e0585207ffe57696af200960..92cfed7aa462d7383c7bdd5d50b383b11cff1d93 100644
--- a/tests/tst_settings.h
+++ b/tests/tst_settings.h
@@ -5,11 +5,18 @@
 
 #include <QObject>
 
+#include "backend/settings.h"
+
 class TestSettings : public QObject
 {
     Q_OBJECT
 private slots:
+    void initTestCase();
 
     void testAutoScaleProject();
     void testDeviceUuid();
+    void testKeepScreenOn();
+
+private:
+    Settings m_settings;
 };