diff --git a/src/app/commoncomponents/LocalVideo.qml b/src/app/commoncomponents/LocalVideo.qml
index 2a8ccd04e9d4ec924ffc8a579c24304389808d09..88e28125a80ca9eae7c4a28b8e4ca8ebf852454b 100644
--- a/src/app/commoncomponents/LocalVideo.qml
+++ b/src/app/commoncomponents/LocalVideo.qml
@@ -23,19 +23,26 @@ import net.jami.Adapters 1.1
 VideoView {
     id: root
 
+    property bool visibilityCondition: true
+
     crop: true
+    visible: isRendering && visibilityCondition
 
     function startWithId(id, force = false) {
         if (id !== undefined && id.length === 0) {
+            stop();
+            return;
+        }
+        const forceRestart = rendererId === id;
+        if (!forceRestart) {
+            // Stop previous device
             VideoDevices.stopDevice(rendererId);
-            rendererId = id;
-        } else {
-            const forceRestart = rendererId === id;
-            if (!forceRestart) {
-                // Stop previous device
-                VideoDevices.stopDevice(rendererId);
-            }
-            rendererId = VideoDevices.startDevice(id, forceRestart);
         }
+        rendererId = VideoDevices.startDevice(id, forceRestart);
+    }
+
+    function stop() {
+        VideoDevices.stopDevice(rendererId);
+        rendererId = "";
     }
 }
diff --git a/src/app/commoncomponents/VideoView.qml b/src/app/commoncomponents/VideoView.qml
index 67d92bad3e28f802eeeb06ef4e64770a1939a6b3..9f345fa377c0cb91bce851273186885ef1e9cd11 100644
--- a/src/app/commoncomponents/VideoView.qml
+++ b/src/app/commoncomponents/VideoView.qml
@@ -29,6 +29,10 @@ Item {
     property real invAspectRatio: (videoOutput.sourceRect.height / videoOutput.sourceRect.width) || 0.5625 // 16:9 default
     property bool crop: false
     property bool flip: false
+    property real blurRadius: 0
+
+    // We need to know if the frames are being rendered to the screen or not.
+    readonly property bool isRendering: videoProvider.activeRenderers[rendererId] !== undefined
 
     // This rect describes the actual rendered content rectangle
     // as the VideoOutput component may use PreserveAspectFit
@@ -55,7 +59,7 @@ Item {
 
         antialiasing: true
         anchors.fill: parent
-        opacity: videoProvider.activeRenderers[rendererId] === true
+        opacity: isRendering
         visible: opacity
 
         fillMode: crop ? VideoOutput.PreserveAspectCrop : VideoOutput.PreserveAspectFit
@@ -70,7 +74,7 @@ Item {
         layer.effect: FastBlur {
             source: videoOutput
             anchors.fill: root
-            radius: (1. - opacity) * 100
+            radius: blurRadius ? blurRadius : (1. - opacity) * 100
         }
 
         transform: Scale {
diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml
index b50e54193b4a94de7716ae3e57149a5e526e263c..26e47c1027041deb86b250f54216d002d84dd678 100644
--- a/src/app/mainview/components/CallOverlay.qml
+++ b/src/app/mainview/components/CallOverlay.qml
@@ -32,6 +32,7 @@ Item {
     id: root
 
     property bool participantsSide: UtilsAdapter.getAppValue(Settings.ParticipantsSide)
+    property alias mainOverlayOpacity: mainOverlay.opacity
 
     signal chatButtonClicked
     signal fullScreenClicked
diff --git a/src/app/mainview/components/InitialCallPage.qml b/src/app/mainview/components/InitialCallPage.qml
index 1ce28c3c0d76a37054451e5c8ec4b467445572c9..4c9448e99fe305fe7e5f9b646c34e45a67176daa 100644
--- a/src/app/mainview/components/InitialCallPage.qml
+++ b/src/app/mainview/components/InitialCallPage.qml
@@ -36,34 +36,28 @@ Rectangle {
     color: "black"
 
     LocalVideo {
-        id: previewRenderer
+        id: localPreview
         anchors.centerIn: parent
         anchors.fill: parent
-        visible: !CurrentCall.isAudioOnly && CurrentAccount.videoEnabled_Video && VideoDevices.listSize !== 0 && ((CurrentCall.status >= Call.Status.INCOMING_RINGING && CurrentCall.status <= Call.Status.SEARCHING) || CurrentCall.status === Call.Status.CONNECTED)
-        opacity: 0.5
 
-        // HACK: this is a workaround to the preview video starting
-        // and stopping a few times. The root cause should be investigated ASAP.
-        Timer {
-            id: controlPreview
-            property bool startVideo
-            interval: 1000
-            onTriggered: {
-                var rendId = visible && startVideo ? VideoDevices.getDefaultDevice() : "";
-                previewRenderer.startWithId(rendId);
+        readonly property bool start: {
+            if (CurrentCall.isAudioOnly || !CurrentAccount.videoEnabled_Video) {
+                return false;
             }
-        }
-        onVisibleChanged: {
-            controlPreview.stop();
-            if (visible) {
-                controlPreview.startVideo = true;
-                controlPreview.interval = 1000;
-            } else {
-                controlPreview.startVideo = false;
-                controlPreview.interval = 0;
+            if (!VideoDevices.listSize) {
+                return false;
             }
-            controlPreview.start();
+            const isCallStatusEligible =
+                (CurrentCall.status >= Call.Status.INCOMING_RINGING &&
+                 CurrentCall.status <= Call.Status.SEARCHING) ||
+                CurrentCall.status === Call.Status.CONNECTED;
+            if (!isCallStatusEligible) {
+                return false;
+            }
+            return true;
         }
+        onStartChanged: localPreview.startWithId(start ? VideoDevices.getDefaultDevice() : "")
+        opacity: 0.5
     }
 
     ListModel {
diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml
index 6eaeadefa74659154a82799c4f85e0ae4fa3d64e..b233c95fa398362ebccd07a5fbba8149a2d00256 100644
--- a/src/app/mainview/components/OngoingCallPage.qml
+++ b/src/app/mainview/components/OngoingCallPage.qml
@@ -41,10 +41,6 @@ Rectangle {
     // A link to the first child will provide access to the chat view.
     property var chatView: chatViewContainer.children[0]
 
-    onCallPreviewIdChanged: {
-        controlPreview.start();
-    }
-
     color: "black"
 
     Connections {
@@ -131,8 +127,8 @@ Rectangle {
 
                 onTapped: function (eventPoint, button) {
                     if (button === Qt.RightButton) {
-                        var isOnLocal = eventPoint.position.x >= previewRenderer.x && eventPoint.position.x <= previewRenderer.x + previewRenderer.width;
-                        isOnLocal &= eventPoint.position.y >= previewRenderer.y && eventPoint.position.y <= previewRenderer.y + previewRenderer.height;
+                        var isOnLocal = eventPoint.position.x >= localPreview.x && eventPoint.position.x <= localPreview.x + localPreview.width;
+                        isOnLocal &= eventPoint.position.y >= localPreview.y && eventPoint.position.y <= localPreview.y + localPreview.height;
                         isOnLocal |= participantsLayer.hoveredOverlaySinkId.indexOf("camera://") === 0;
                         callOverlay.openCallViewContextMenuInPos(eventPoint.position.x, eventPoint.position.y, participantsLayer.hoveredOverlayUri, participantsLayer.hoveredOverlaySinkId, participantsLayer.hoveredOverVideoMuted, isOnLocal);
                     }
@@ -171,53 +167,90 @@ Rectangle {
             }
 
             LocalVideo {
-                id: previewRenderer
+                id: localPreview
                 objectName: "localPreview"
 
-                visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference
+                readonly property var container: parent
+                readonly property string callPreviewId: root.callPreviewId
+
+                visibilityCondition: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) &&
+                                     !CurrentCall.isConference
                 height: width * invAspectRatio
-                width: Math.max(callPageMainRect.width / 5, JamiTheme.minimumPreviewWidth)
-                x: callPageMainRect.width - previewRenderer.width - previewMargin
-                y: previewMarginYTop
+                width: Math.max(container.width / 5, JamiTheme.minimumPreviewWidth)
                 flip: CurrentCall.flipSelf && !CurrentCall.isSharing
+                blurRadius: hidden ? 25 : 0
+                onCallPreviewIdChanged: startWithId(callPreviewId)
+                onVisibleChanged: if (!visible) stop()
 
-                // HACK: this is a workaround to the preview video starting
-                // and stopping a few times. The root cause should be investigated ASAP.
-                Timer {
-                    id: controlPreview
-                    property bool startVideo
-                    interval: 1000
-                    onTriggered: {
-                        var rendId = visible && startVideo ? root.callPreviewId : "";
-                        previewRenderer.startWithId(rendId);
-                    }
-                }
+                anchors.topMargin: previewMarginYTop
+                anchors.leftMargin: sideMargin
+                anchors.rightMargin: sideMargin
+                anchors.bottomMargin: previewMarginYBottom
 
-                onVisibleChanged: {
-                    controlPreview.stop();
-                    if (visible) {
-                        controlPreview.startVideo = true;
-                        controlPreview.interval = 1000;
-                    } else {
-                        controlPreview.startVideo = false;
-                        controlPreview.interval = 0;
+                opacity: hidden ? callOverlay.mainOverlayOpacity : 1
+
+                // Allow hiding the preview (available when anchored)
+                readonly property bool anchored: state !== "unanchored"
+                property bool hidden: false
+                readonly property real hiddenHandleSize: 32
+                // Compute the margin as a function of the preview width in order to
+                // apply a negative margin and expose a constant width handle.
+                // If not hidden, return the previewMargin.
+                property real sideMargin: !hidden ? previewMargin : -(width - hiddenHandleSize)
+                // Animate the hiddenSize with a Behavior.
+                Behavior on sideMargin { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
+                readonly property bool onLeft: state.indexOf("left") !== -1
+                PushButton {
+                    id: hidePreviewButton
+                    objectName: "hidePreviewButton"
+
+                    width: localPreview.hiddenHandleSize
+                    state: localPreview.onLeft ?
+                               (localPreview.hidden ? "right" : "left") :
+                               (localPreview.hidden ? "left" : "right")
+                    states: [
+                        State {
+                            name: "left"
+                            AnchorChanges {
+                                target: hidePreviewButton
+                                anchors.left: parent.left
+                            }
+                        },
+                        State {
+                            name: "right"
+                            AnchorChanges {
+                                target: hidePreviewButton
+                                anchors.right: parent.right
+                            }
+                        }
+                    ]
+                    anchors.top: parent.top
+                    anchors.bottom: parent.bottom
+                    opacity: (localPreview.anchored && hoverHandler.hovered) || localPreview.hidden
+                    Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
+                    visible: opacity > 0
+                    background: Rectangle {
+                        readonly property color normalColor: JamiTheme.mediumGrey
+                        color: JamiTheme.mediumGrey
+                        opacity: hidePreviewButton.hovered ? 0.7 : 0.5
+                        Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
                     }
-                    controlPreview.start();
+                    normalImageSource: hidePreviewButton.state === "left" ?
+                                           JamiResources.chevron_left_black_24dp_svg :
+                                           JamiResources.chevron_right_black_24dp_svg
+                    imageColor: JamiTheme.darkGreyColor
+                    onClicked: localPreview.hidden = !localPreview.hidden
+                    toolTipText: localPreview.hidden ?
+                                     JamiStrings.showLocalVideo :
+                                     JamiStrings.hideLocalVideo
                 }
 
-                anchors.topMargin: previewMarginYTop
-                anchors.leftMargin: previewMargin
-                anchors.rightMargin: previewMargin
-                anchors.bottomMargin: previewMarginYBottom
-
                 state: "anchor_top_right"
-
                 states: [
-                    // Not anchored
                     State {
                         name: "unanchored"
                         AnchorChanges {
-                            target: previewRenderer
+                            target: localPreview
                             anchors.top: undefined
                             anchors.right: undefined
                             anchors.bottom: undefined
@@ -227,33 +260,33 @@ Rectangle {
                     State {
                         name: "anchor_top_left"
                         AnchorChanges {
-                            target: previewRenderer
-                            anchors.top: callPageMainRect.top
-                            anchors.left: callPageMainRect.left
+                            target: localPreview
+                            anchors.top: localPreview.container.top
+                            anchors.left: localPreview.container.left
                         }
                     },
                     State {
                         name: "anchor_top_right"
                         AnchorChanges {
-                            target: previewRenderer
-                            anchors.top: callPageMainRect.top
-                            anchors.right: callPageMainRect.right
+                            target: localPreview
+                            anchors.top: localPreview.container.top
+                            anchors.right: localPreview.container.right
                         }
                     },
                     State {
                         name: "anchor_bottom_right"
                         AnchorChanges {
-                            target: previewRenderer
-                            anchors.bottom: callPageMainRect.bottom
-                            anchors.right: callPageMainRect.right
+                            target: localPreview
+                            anchors.bottom: localPreview.container.bottom
+                            anchors.right: localPreview.container.right
                         }
                     },
                     State {
                         name: "anchor_bottom_left"
                         AnchorChanges {
-                            target: previewRenderer
-                            anchors.bottom: callPageMainRect.bottom
-                            anchors.left: callPageMainRect.left
+                            target: localPreview
+                            anchors.bottom: localPreview.container.bottom
+                            anchors.left: localPreview.container.left
                         }
                     }
                 ]
@@ -266,17 +299,23 @@ Rectangle {
                     }
                 }
 
+                HoverHandler {
+                    id: hoverHandler
+                }
+
                 DragHandler {
-                    readonly property var container: callPageMainRect
+                    id: dragHandler
+                    readonly property var container: localPreview.container
                     target: parent
                     dragThreshold: 4
+                    enabled: !localPreview.hidden
                     xAxis.maximum: container.width - parent.width - previewMargin
                     xAxis.minimum: previewMargin
                     yAxis.maximum: container.height - parent.height - previewMarginYBottom
                     yAxis.minimum: previewMarginYTop
                     onActiveChanged: {
                         if (active) {
-                            previewRenderer.state = "unanchored";
+                            localPreview.state = "unanchored";
                         } else {
                             const center = Qt.point(target.x + target.width / 2,
                                                     target.y + target.height / 2);
@@ -284,15 +323,15 @@ Rectangle {
                                                              container.y + container.height / 2);
                             if (center.x >= containerCenter.x) {
                                 if (center.y >= containerCenter.y) {
-                                    previewRenderer.state = "anchor_bottom_right";
+                                    localPreview.state = "anchor_bottom_right";
                                 } else {
-                                    previewRenderer.state = "anchor_top_right";
+                                    localPreview.state = "anchor_top_right";
                                 }
                             } else {
                                 if (center.y >= containerCenter.y) {
-                                    previewRenderer.state = "anchor_bottom_left";
+                                    localPreview.state = "anchor_bottom_left";
                                 } else {
-                                    previewRenderer.state = "anchor_top_left";
+                                    localPreview.state = "anchor_top_left";
                                 }
                             }
                         }
@@ -302,8 +341,8 @@ Rectangle {
                 layer.enabled: true
                 layer.effect: OpacityMask {
                     maskSource: Rectangle {
-                        width: previewRenderer.width
-                        height: previewRenderer.height
+                        width: localPreview.width
+                        height: localPreview.height
                         radius: JamiTheme.primaryRadius
                     }
                 }
diff --git a/src/app/net/jami/Constants/JamiStrings.qml b/src/app/net/jami/Constants/JamiStrings.qml
index 611564a2275cdd8d6be0e7bbaad3e0710b2c5b1f..3316ed0797091a94e4e105e1d7f72892cdf176bf 100644
--- a/src/app/net/jami/Constants/JamiStrings.qml
+++ b/src/app/net/jami/Constants/JamiStrings.qml
@@ -785,6 +785,8 @@ Item {
     property string becomeHostOneCall: qsTr("Host only this call")
     property string hostThisCall: qsTr("Host this call")
     property string becomeDefaultHost: qsTr("Make me the default host for future calls")
+    property string showLocalVideo: qsTr("Show local video")
+    property string hideLocalVideo: qsTr("Hide local video")
 
     // Invitation View
     property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.")
diff --git a/src/app/videodevices.cpp b/src/app/videodevices.cpp
index a986c4f4b058e9269eb843f10fcc40a8db58ae6f..86eb9eed3f4359784befd2391b50a077bffa9e69 100644
--- a/src/app/videodevices.cpp
+++ b/src/app/videodevices.cpp
@@ -257,8 +257,12 @@ void
 VideoDevices::stopDevice(const QString& id)
 {
     if (!id.isEmpty()) {
-        lrcInstance_->avModel().stopPreview(id);
-        deviceOpen_ = false;
+        qInfo() << "Stopping device" << id;
+        if (lrcInstance_->avModel().stopPreview(id)) {
+            deviceOpen_ = false;
+        } else {
+            qWarning() << "Failed to stop device" << id;
+        }
     }
 }
 
diff --git a/src/libclient/api/avmodel.h b/src/libclient/api/avmodel.h
index fc4e277d16d4775d6d01f30bd5395fad04c1a46d..bb22c5c1b5d24891df226a8d42b95abee84047cc 100644
--- a/src/libclient/api/avmodel.h
+++ b/src/libclient/api/avmodel.h
@@ -243,7 +243,7 @@ public:
      * Stop preview renderer and the camera.
      * @param resource
      */
-    Q_INVOKABLE void stopPreview(const QString& resource);
+    Q_INVOKABLE bool stopPreview(const QString& resource);
     /**
      * Get the list of available windows ids
      * X11: a id is of the form 0x0000000
diff --git a/src/libclient/avmodel.cpp b/src/libclient/avmodel.cpp
index 214a514e234eaea1e0f96dfc89aeaa43a778be1a..3712bfcb1660e42a3316093711398b4869144d50 100644
--- a/src/libclient/avmodel.cpp
+++ b/src/libclient/avmodel.cpp
@@ -560,10 +560,10 @@ AVModel::startPreview(const QString& resource)
     return VideoManager::instance().openVideoInput(resource);
 }
 
-void
+bool
 AVModel::stopPreview(const QString& resource)
 {
-    VideoManager::instance().closeVideoInput(resource);
+    return VideoManager::instance().closeVideoInput(resource);
 }
 
 #ifdef WIN32
@@ -795,42 +795,50 @@ AVModel::useDirectRenderer() const
 #endif
 }
 
-QString AVModel::createMediaPlayer(const QString& resource)
+QString
+AVModel::createMediaPlayer(const QString& resource)
 {
     return VideoManager::instance().createMediaPlayer(resource);
 }
 
-void AVModel::closeMediaPlayer(const QString& resource)
+void
+AVModel::closeMediaPlayer(const QString& resource)
 {
     VideoManager::instance().closeMediaPlayer(resource);
 }
 
-bool AVModel::pausePlayer(const QString& id, bool pause)
+bool
+AVModel::pausePlayer(const QString& id, bool pause)
 {
     return VideoManager::instance().pausePlayer(id, pause);
 }
 
-bool AVModel::mutePlayerAudio(const QString& id, bool mute)
+bool
+AVModel::mutePlayerAudio(const QString& id, bool mute)
 {
     return VideoManager::instance().mutePlayerAudio(id, mute);
 }
 
-bool AVModel::playerSeekToTime(const QString& id, int time)
+bool
+AVModel::playerSeekToTime(const QString& id, int time)
 {
     return VideoManager::instance().playerSeekToTime(id, time);
 }
 
-qint64 AVModel::getPlayerPosition(const QString& id)
+qint64
+AVModel::getPlayerPosition(const QString& id)
 {
     return VideoManager::instance().getPlayerPosition(id);
 }
 
-qint64 AVModel::getPlayerDuration(const QString& id)
+qint64
+AVModel::getPlayerDuration(const QString& id)
 {
     return VideoManager::instance().getPlayerDuration(id);
 }
 
-void AVModel::setAutoRestart(const QString& id, bool restart)
+void
+AVModel::setAutoRestart(const QString& id, bool restart)
 {
     VideoManager::instance().setAutoRestart(id, restart);
 }
diff --git a/src/libclient/qtwrapper/videomanager_wrap.h b/src/libclient/qtwrapper/videomanager_wrap.h
index 9625c2b91a7cb029ed47fe9db2add9ef4f69c60d..88601c0bf66002183afb732e259280a506e5da59 100644
--- a/src/libclient/qtwrapper/videomanager_wrap.h
+++ b/src/libclient/qtwrapper/videomanager_wrap.h
@@ -130,10 +130,10 @@ public Q_SLOTS: // METHODS
 #endif
     }
 
-    void closeVideoInput(const QString& resource)
+    bool closeVideoInput(const QString& resource)
     {
 #ifdef ENABLE_VIDEO
-        libjami::closeVideoInput(resource.toLatin1().toStdString());
+        return libjami::closeVideoInput(resource.toLatin1().toStdString());
 #endif
     }
 
diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp
index d652a63edb8a400efd96b0bd58067a72bdff161e..ef1617ddac6f20cf9b93e46cc1594dcc4097b392 100644
--- a/tests/qml/main.cpp
+++ b/tests/qml/main.cpp
@@ -88,11 +88,9 @@ public Q_SLOTS:
         auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath);
         lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/";
 
-        // Create 2 Account
-        QSignalSpy accountStatusChangedSpy(&lrcInstance_->accountModel(),
-                                           &AccountModel::accountStatusChanged);
-
+        // Create 2 Accounts
         QSignalSpy accountAddedSpy(&lrcInstance_->accountModel(), &AccountModel::accountAdded);
+
         aliceId = lrcInstance_->accountModel().createNewAccount(profile::Type::JAMI, "Alice");
         accountAddedSpy.wait(15000);
         QCOMPARE(accountAddedSpy.count(), 1);
diff --git a/tests/qml/src/TestWrapper.qml b/tests/qml/src/TestWrapper.qml
index 0f50f3f8af329f70d12fdabb3509cab034cdec63..80dffc31d63be01df2f0f7be162d082673984451 100644
--- a/tests/qml/src/TestWrapper.qml
+++ b/tests/qml/src/TestWrapper.qml
@@ -32,6 +32,20 @@ Item {
     width: childrenRect.width
     height: childrenRect.height
 
+    // This is a helper function to wait for a signal to be emitted and check a condition.
+    function waitForSignalAndCheck(signalObject, signalName, action, checkExpression) {
+        // Create the SignalSpy component dynamically with the provided signal object and name.
+        const spy = Qt.createQmlObject('import QtTest 1.0; SignalSpy {}', this);
+        spy.target = signalObject;
+        spy.signalName = signalName;
+        // Perform the action that should emit the signal.
+        action();
+        // Wait a maximum of 1 second for the signal to be emitted.
+        spy.wait(1000);
+        // Check the signal count and the provided expression.
+        return spy.count > 0 && checkExpression();
+    }
+
     // A binding to the windowShown property
     Binding {
         tw.appWindow: uut.Window.window
diff --git a/tests/qml/src/tst_OngoingCallPage.qml b/tests/qml/src/tst_OngoingCallPage.qml
index 8d3e06b3068399bbe35289d5a8e5230bc1d2c88b..6ff04036cd0c5cfbf12fcd83fe0633856e87d0db 100644
--- a/tests/qml/src/tst_OngoingCallPage.qml
+++ b/tests/qml/src/tst_OngoingCallPage.qml
@@ -38,7 +38,12 @@ TestWrapper {
             when: windowShown // Mouse events can only be handled
                               // after the window has been shown.
 
+            property string dummyImgUrl
+
             function initTestCase() {
+                // Create a dummy image file to use for the local preview.
+                dummyImgUrl = UtilsAdapter.urlFromLocalPath(UtilsAdapter.createDummyImage());
+
                 // The CallActionBar on the OngoingCallPage starts out invisible and
                 // is made visible whenever the user moves their mouse.
                 // This is implemented via an event filter in the CallOverlayModel
@@ -79,50 +84,88 @@ TestWrapper {
                 compare(callActionBar.visible, true)
             }
 
-            function test_checkLocalPreviewAnchoring() {
+            // Define a generic local preview test wrapper function that starts and stops
+            // the local preview, and calls the provided test function in between.
+            function localPreviewTestWrapper(testFunction) {
                 const localPreview = findChild(uut, "localPreview");
-                const container = localPreview.parent;
-
-                // The preview should normally be invisible at first, but there is a bug
-                // in the current implementation that makes it visible. This will need to
-                // be adjusted once the bug is fixed.
-                compare(localPreview.visible, true);
-
-                // Start a preview of a local resource.
-                const dummyImgFile = UtilsAdapter.createDummyImage();
-                localPreview.startWithId(UtilsAdapter.urlFromLocalPath(dummyImgFile));
-
-                // First check that the preview is anchored.
-                verify(localPreview.state.indexOf("unanchored") === -1);
-
-                const containerCenter = Qt.point(container.width / 2, container.height / 2);
-                function moveAndVerifyState(dx, dy, expectedState) {
-                    const previewCenter = Qt.point(localPreview.x + localPreview.width / 2,
-                                                   localPreview.y + localPreview.height / 2);
-                    const destination = Qt.point(containerCenter.x + dx,
-                                                 containerCenter.y + dy);
-                    // Position the mouse at the center of the preview, then drag it until
-                    // we reach the destination point.
-                    mouseDrag(container, previewCenter.x, previewCenter.y,
-                              destination.x - previewCenter.x, destination.y - previewCenter.y);
-                    wait(250);
-                    compare(localPreview.state, expectedState);
-                }
-
-                const dx = 1;
-                const dy = 1;
-                moveAndVerifyState(-dx, -dy, "anchor_top_left");
-                moveAndVerifyState(dx, -dy, "anchor_top_right");
-                moveAndVerifyState(-dx, dy, "anchor_bottom_left");
-                moveAndVerifyState(dx, dy, "anchor_bottom_right");
-
-                // Verify that during a drag process, the preview is unanchored.
-                mousePress(localPreview);
-                mouseMove(localPreview, 100, 100);
-                verify(localPreview.state.indexOf("unanchored") !== -1);
+
+                // The preview should be invisible at first.
+                compare(localPreview.visible, false);
+
+                // Start a preview of a local resource and wait for the preview to become visible.
+                verify(waitForSignalAndCheck(localPreview,
+                                             "visibleChanged",
+                                             () => localPreview.startWithId(dummyImgUrl),
+                                             () => localPreview.visible));
+
+                // Call the provided test function.
+                testFunction(localPreview);
 
                 // Stop the preview.
-                localPreview.startWithId("");
+                verify(waitForSignalAndCheck(localPreview,
+                                             "visibleChanged",
+                                             () => localPreview.stop(),
+                                             () => !localPreview.visible));
+            }
+
+            function test_localPreviewAnchoring() {
+                localPreviewTestWrapper(function(localPreview) {
+                    const container = localPreview.parent;
+
+                    // First check that the preview is anchored.
+                    verify(localPreview.state.indexOf("unanchored") === -1);
+
+                    const containerCenter = Qt.point(container.width / 2, container.height / 2);
+                    function moveAndVerifyState(dx, dy, expectedState) {
+                        const previewCenter = Qt.point(localPreview.x + localPreview.width / 2,
+                                                       localPreview.y + localPreview.height / 2);
+                        const destination = Qt.point(containerCenter.x + dx,
+                                                     containerCenter.y + dy);
+                        // Position the mouse at the center of the preview, then drag it until
+                        // we reach the destination point.
+                        mouseDrag(container, previewCenter.x, previewCenter.y,
+                                  destination.x - previewCenter.x, destination.y - previewCenter.y);
+                        wait(250);
+                        compare(localPreview.state, expectedState);
+                    }
+
+                    const dx = 1;
+                    const dy = 1;
+                    moveAndVerifyState(-dx, -dy, "anchor_top_left");
+                    moveAndVerifyState(dx, -dy, "anchor_top_right");
+                    moveAndVerifyState(-dx, dy, "anchor_bottom_left");
+                    moveAndVerifyState(dx, dy, "anchor_bottom_right");
+
+                    // Verify that during a drag process, the preview is unanchored.
+                    mousePress(localPreview);
+                    mouseMove(localPreview, 100, 100);
+                    verify(localPreview.state.indexOf("unanchored") !== -1);
+                    mouseRelease(localPreview);
+                });
+            }
+
+            function test_localPreviewHiding() {
+                localPreviewTestWrapper(function(localPreview) {
+                    // Make sure the preview is anchored.
+                    verify(localPreview.state.indexOf("unanchored") === -1);
+
+                    // It should also not be hidden.
+                    compare(localPreview.hidden, false);
+
+                    // We presume that the preview is anchored and that once we hover over the
+                    // local preview, that the hide button will become visible.
+                    // Note: There is currently an issue where the CallOverlay is detecting hover
+                    // events, but child controls are not. This should be addressed as it seems
+                    // to affect MouseArea, HoverHandler, Buttons, and other controls that rely
+                    // on hover events. For now, we'll manually trigger the onClicked event.
+                    const hidePreviewButton = findChild(localPreview, "hidePreviewButton");
+                    hidePreviewButton.onClicked();
+                    compare(localPreview.hidden, true);
+
+                    // "Click" the hide button again to unhide the preview.
+                    hidePreviewButton.onClicked();
+                    compare(localPreview.hidden, false);
+                });
             }
         }
     }