Add tests for linking a new device. (#4934)

* Replace GrantLoginWithQrCodeHandlerSDKMock with LinkNewDeviceServiceMock.

Add tests for all initial states on the QRCodeLoginScreen.

* Add tests for linking both mobile and desktop devices.

* Add UI tests for linking a new device.

* Don't show the Link Desktop Computer button when running on macOS.

This mirrors the decision to hide the Sign In With QR Code button on the start screen.
This commit is contained in:
Doug
2026-01-09 13:10:14 +00:00
committed by GitHub
parent 00dc901ce1
commit 7cb472a4f7
21 changed files with 409 additions and 93 deletions
@@ -106,6 +106,7 @@ extension ClientProxyMock {
resetIdentityReturnValue = .success(IdentityResetHandleSDKMock(.init()))
spaceService = SpaceServiceProxyMock(configuration.spaceServiceConfiguration)
linkNewDeviceServiceReturnValue = LinkNewDeviceServiceMock(.init())
roomForIdentifierClosure = { [weak self] identifier in
if let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) {
@@ -4049,13 +4049,13 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
return linkNewDeviceServiceCallsCount > 0
}
var linkNewDeviceServiceUnderlyingReturnValue: LinkNewDeviceService!
var linkNewDeviceServiceReturnValue: LinkNewDeviceService! {
var linkNewDeviceServiceUnderlyingReturnValue: LinkNewDeviceServiceProtocol!
var linkNewDeviceServiceReturnValue: LinkNewDeviceServiceProtocol! {
get {
if Thread.isMainThread {
return linkNewDeviceServiceUnderlyingReturnValue
} else {
var returnValue: LinkNewDeviceService? = nil
var returnValue: LinkNewDeviceServiceProtocol? = nil
DispatchQueue.main.sync {
returnValue = linkNewDeviceServiceUnderlyingReturnValue
}
@@ -4073,9 +4073,9 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
}
}
var linkNewDeviceServiceClosure: (() -> LinkNewDeviceService)?
var linkNewDeviceServiceClosure: (() -> LinkNewDeviceServiceProtocol)?
func linkNewDeviceService() -> LinkNewDeviceService {
func linkNewDeviceService() -> LinkNewDeviceServiceProtocol {
linkNewDeviceServiceCallsCount += 1
if let linkNewDeviceServiceClosure = linkNewDeviceServiceClosure {
return linkNewDeviceServiceClosure()
@@ -10917,6 +10917,143 @@ class KnockedRoomProxyMock: KnockedRoomProxyProtocol, @unchecked Sendable {
}
}
}
class LinkNewDeviceServiceMock: LinkNewDeviceServiceProtocol, @unchecked Sendable {
//MARK: - linkMobileDevice
var linkMobileDeviceUnderlyingCallsCount = 0
var linkMobileDeviceCallsCount: Int {
get {
if Thread.isMainThread {
return linkMobileDeviceUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = linkMobileDeviceUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
linkMobileDeviceUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
linkMobileDeviceUnderlyingCallsCount = newValue
}
}
}
}
var linkMobileDeviceCalled: Bool {
return linkMobileDeviceCallsCount > 0
}
var linkMobileDeviceUnderlyingReturnValue: LinkNewDeviceService.LinkMobileProgressPublisher!
var linkMobileDeviceReturnValue: LinkNewDeviceService.LinkMobileProgressPublisher! {
get {
if Thread.isMainThread {
return linkMobileDeviceUnderlyingReturnValue
} else {
var returnValue: LinkNewDeviceService.LinkMobileProgressPublisher? = nil
DispatchQueue.main.sync {
returnValue = linkMobileDeviceUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
linkMobileDeviceUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
linkMobileDeviceUnderlyingReturnValue = newValue
}
}
}
}
var linkMobileDeviceClosure: (() -> LinkNewDeviceService.LinkMobileProgressPublisher)?
func linkMobileDevice() -> LinkNewDeviceService.LinkMobileProgressPublisher {
linkMobileDeviceCallsCount += 1
if let linkMobileDeviceClosure = linkMobileDeviceClosure {
return linkMobileDeviceClosure()
} else {
return linkMobileDeviceReturnValue
}
}
//MARK: - linkDesktopDevice
var linkDesktopDeviceWithUnderlyingCallsCount = 0
var linkDesktopDeviceWithCallsCount: Int {
get {
if Thread.isMainThread {
return linkDesktopDeviceWithUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = linkDesktopDeviceWithUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
linkDesktopDeviceWithUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
linkDesktopDeviceWithUnderlyingCallsCount = newValue
}
}
}
}
var linkDesktopDeviceWithCalled: Bool {
return linkDesktopDeviceWithCallsCount > 0
}
var linkDesktopDeviceWithReceivedScannedQRData: Data?
var linkDesktopDeviceWithReceivedInvocations: [Data] = []
var linkDesktopDeviceWithUnderlyingReturnValue: LinkNewDeviceService.LinkDesktopProgressPublisher!
var linkDesktopDeviceWithReturnValue: LinkNewDeviceService.LinkDesktopProgressPublisher! {
get {
if Thread.isMainThread {
return linkDesktopDeviceWithUnderlyingReturnValue
} else {
var returnValue: LinkNewDeviceService.LinkDesktopProgressPublisher? = nil
DispatchQueue.main.sync {
returnValue = linkDesktopDeviceWithUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
linkDesktopDeviceWithUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
linkDesktopDeviceWithUnderlyingReturnValue = newValue
}
}
}
}
var linkDesktopDeviceWithClosure: ((Data) -> LinkNewDeviceService.LinkDesktopProgressPublisher)?
func linkDesktopDevice(with scannedQRData: Data) -> LinkNewDeviceService.LinkDesktopProgressPublisher {
linkDesktopDeviceWithCallsCount += 1
linkDesktopDeviceWithReceivedScannedQRData = scannedQRData
DispatchQueue.main.async {
self.linkDesktopDeviceWithReceivedInvocations.append(scannedQRData)
}
if let linkDesktopDeviceWithClosure = linkDesktopDeviceWithClosure {
return linkDesktopDeviceWithClosure(scannedQRData)
} else {
return linkDesktopDeviceWithReturnValue
}
}
}
class MediaLoaderMock: MediaLoaderProtocol, @unchecked Sendable {
//MARK: - loadMediaContentForSource
@@ -0,0 +1,33 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftUI
extension LinkNewDeviceServiceMock {
static var mockQRCodeImage: UIImage {
mockBase64QRCode.data(using: .utf8).flatMap { UIImage(qrCodeData: $0) } ?? UIImage()
}
static let mockBase64QRCode = """
TUFUUklYAgS0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PABaaHR0
cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2L19zeW5hcHNlL2Ns
aWVudC9yZW5kZXp2b3VzLzAxSFg5SzAwUTFINktQRDQ3RUc0RzFUM1hHACVo
dHRwczovL3N5bmFwc2Utb2lkYy5sYWIuZWxlbWVudC5kZXYv
"""
struct Configuration {
var linkMobileProgressPublisher: LinkNewDeviceService.LinkMobileProgressPublisher = .init(.starting)
var linkDesktopProgressPublisher: LinkNewDeviceService.LinkDesktopProgressPublisher = .init(.starting)
}
convenience init(_ configuration: Configuration) {
self.init()
linkMobileDeviceReturnValue = configuration.linkMobileProgressPublisher
linkDesktopDeviceWithReturnValue = configuration.linkDesktopProgressPublisher
}
}
@@ -1,34 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
import MatrixRustSDKMocks
extension GrantLoginWithQrCodeHandlerSDKMock {
struct Configuration {
var generateDelay: Duration = .seconds(0)
var generatedBase64QRCode = """
TUFUUklYAgS0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PABaaHR0
cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2L19zeW5hcHNlL2Ns
aWVudC9yZW5kZXp2b3VzLzAxSFg5SzAwUTFINktQRDQ3RUc0RzFUM1hHACVo
dHRwczovL3N5bmFwc2Utb2lkYy5sYWIuZWxlbWVudC5kZXYv
"""
}
convenience init(_ configuration: Configuration) {
self.init()
generateProgressListenerClosure = { listener in
Task {
try await Task.sleep(for: configuration.generateDelay)
let bytes = Data(base64Encoded: configuration.generatedBase64QRCode) ?? Data()
try listener.onUpdate(state: .qrReady(qrCode: .fromBytes(bytes: bytes)))
}
}
}
}