add NSE sources to EX project to easily implement mocking and allow the code to be unit tested

This commit is contained in:
Mauro Romito
2025-11-18 14:37:32 +01:00
committed by Mauro
parent b67b4fef2f
commit 8e52ef363b
12 changed files with 457 additions and 29 deletions
@@ -11556,6 +11556,188 @@ class MediaProviderMock: MediaProviderProtocol, @unchecked Sendable {
}
}
}
class NSEUserSessionMock: NSEUserSessionProtocol, @unchecked Sendable {
var inviteAvatarsVisibilityCallsCount = 0
var inviteAvatarsVisibilityCalled: Bool {
return inviteAvatarsVisibilityCallsCount > 0
}
var inviteAvatarsVisibility: InviteAvatars {
get async {
inviteAvatarsVisibilityCallsCount += 1
if let inviteAvatarsVisibilityClosure = inviteAvatarsVisibilityClosure {
return await inviteAvatarsVisibilityClosure()
} else {
return underlyingInviteAvatarsVisibility
}
}
}
var underlyingInviteAvatarsVisibility: InviteAvatars!
var inviteAvatarsVisibilityClosure: (() async -> InviteAvatars)?
var mediaPreviewVisibilityCallsCount = 0
var mediaPreviewVisibilityCalled: Bool {
return mediaPreviewVisibilityCallsCount > 0
}
var mediaPreviewVisibility: MediaPreviews {
get async {
mediaPreviewVisibilityCallsCount += 1
if let mediaPreviewVisibilityClosure = mediaPreviewVisibilityClosure {
return await mediaPreviewVisibilityClosure()
} else {
return underlyingMediaPreviewVisibility
}
}
}
var underlyingMediaPreviewVisibility: MediaPreviews!
var mediaPreviewVisibilityClosure: (() async -> MediaPreviews)?
var threadsEnabled: Bool {
get { return underlyingThreadsEnabled }
set(value) { underlyingThreadsEnabled = value }
}
var underlyingThreadsEnabled: Bool!
//MARK: - notificationItemProxy
var notificationItemProxyRoomIDEventIDUnderlyingCallsCount = 0
var notificationItemProxyRoomIDEventIDCallsCount: Int {
get {
if Thread.isMainThread {
return notificationItemProxyRoomIDEventIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = notificationItemProxyRoomIDEventIDUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
notificationItemProxyRoomIDEventIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
notificationItemProxyRoomIDEventIDUnderlyingCallsCount = newValue
}
}
}
}
var notificationItemProxyRoomIDEventIDCalled: Bool {
return notificationItemProxyRoomIDEventIDCallsCount > 0
}
var notificationItemProxyRoomIDEventIDReceivedArguments: (roomID: String, eventID: String)?
var notificationItemProxyRoomIDEventIDReceivedInvocations: [(roomID: String, eventID: String)] = []
var notificationItemProxyRoomIDEventIDUnderlyingReturnValue: NotificationItemProxyProtocol?
var notificationItemProxyRoomIDEventIDReturnValue: NotificationItemProxyProtocol? {
get {
if Thread.isMainThread {
return notificationItemProxyRoomIDEventIDUnderlyingReturnValue
} else {
var returnValue: NotificationItemProxyProtocol?? = nil
DispatchQueue.main.sync {
returnValue = notificationItemProxyRoomIDEventIDUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
notificationItemProxyRoomIDEventIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
notificationItemProxyRoomIDEventIDUnderlyingReturnValue = newValue
}
}
}
}
var notificationItemProxyRoomIDEventIDClosure: ((String, String) async -> NotificationItemProxyProtocol?)?
func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? {
notificationItemProxyRoomIDEventIDCallsCount += 1
notificationItemProxyRoomIDEventIDReceivedArguments = (roomID: roomID, eventID: eventID)
DispatchQueue.main.async {
self.notificationItemProxyRoomIDEventIDReceivedInvocations.append((roomID: roomID, eventID: eventID))
}
if let notificationItemProxyRoomIDEventIDClosure = notificationItemProxyRoomIDEventIDClosure {
return await notificationItemProxyRoomIDEventIDClosure(roomID, eventID)
} else {
return notificationItemProxyRoomIDEventIDReturnValue
}
}
//MARK: - roomForIdentifier
var roomForIdentifierUnderlyingCallsCount = 0
var roomForIdentifierCallsCount: Int {
get {
if Thread.isMainThread {
return roomForIdentifierUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = roomForIdentifierUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
roomForIdentifierUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
roomForIdentifierUnderlyingCallsCount = newValue
}
}
}
}
var roomForIdentifierCalled: Bool {
return roomForIdentifierCallsCount > 0
}
var roomForIdentifierReceivedRoomID: String?
var roomForIdentifierReceivedInvocations: [String] = []
var roomForIdentifierUnderlyingReturnValue: Room?
var roomForIdentifierReturnValue: Room? {
get {
if Thread.isMainThread {
return roomForIdentifierUnderlyingReturnValue
} else {
var returnValue: Room?? = nil
DispatchQueue.main.sync {
returnValue = roomForIdentifierUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
roomForIdentifierUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
roomForIdentifierUnderlyingReturnValue = newValue
}
}
}
}
var roomForIdentifierClosure: ((String) -> Room?)?
func roomForIdentifier(_ roomID: String) -> Room? {
roomForIdentifierCallsCount += 1
roomForIdentifierReceivedRoomID = roomID
DispatchQueue.main.async {
self.roomForIdentifierReceivedInvocations.append(roomID)
}
if let roomForIdentifierClosure = roomForIdentifierClosure {
return roomForIdentifierClosure(roomID)
} else {
return roomForIdentifierReturnValue
}
}
}
class NetworkMonitorMock: NetworkMonitorProtocol, @unchecked Sendable {
var reachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
get { return underlyingReachabilityPublisher }
@@ -11563,6 +11745,59 @@ class NetworkMonitorMock: NetworkMonitorProtocol, @unchecked Sendable {
}
var underlyingReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>!
}
class NotificationItemProxyMock: NotificationItemProxyProtocol, @unchecked Sendable {
var event: NotificationEvent?
var senderID: String {
get { return underlyingSenderID }
set(value) { underlyingSenderID = value }
}
var underlyingSenderID: String!
var roomID: String {
get { return underlyingRoomID }
set(value) { underlyingRoomID = value }
}
var underlyingRoomID: String!
var receiverID: String {
get { return underlyingReceiverID }
set(value) { underlyingReceiverID = value }
}
var underlyingReceiverID: String!
var senderDisplayName: String?
var senderAvatarMediaSource: MediaSourceProxy?
var roomDisplayName: String {
get { return underlyingRoomDisplayName }
set(value) { underlyingRoomDisplayName = value }
}
var underlyingRoomDisplayName: String!
var roomAvatarMediaSource: MediaSourceProxy?
var roomJoinedMembers: Int {
get { return underlyingRoomJoinedMembers }
set(value) { underlyingRoomJoinedMembers = value }
}
var underlyingRoomJoinedMembers: Int!
var isRoomDirect: Bool {
get { return underlyingIsRoomDirect }
set(value) { underlyingIsRoomDirect = value }
}
var underlyingIsRoomDirect: Bool!
var isRoomPrivate: Bool {
get { return underlyingIsRoomPrivate }
set(value) { underlyingIsRoomPrivate = value }
}
var underlyingIsRoomPrivate: Bool!
var isNoisy: Bool {
get { return underlyingIsNoisy }
set(value) { underlyingIsNoisy = value }
}
var underlyingIsNoisy: Bool!
var hasMention: Bool {
get { return underlyingHasMention }
set(value) { underlyingHasMention = value }
}
var underlyingHasMention: Bool!
var threadRootEventID: String?
}
class NotificationManagerMock: NotificationManagerProtocol, @unchecked Sendable {
weak var delegate: NotificationManagerDelegate?
@@ -0,0 +1,24 @@
//
// 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 MatrixRustSDK
struct NSEUserSessionMockConfiguration {
var inviteAvatarsVisibility = InviteAvatars.on
var mediaPreviewVisibility = MediaPreviews.on
var threadsEnabled = true
}
extension NSEUserSessionMock {
convenience init(_ configuration: NSEUserSessionMockConfiguration) {
self.init()
underlyingInviteAvatarsVisibility = configuration.inviteAvatarsVisibility
underlyingMediaPreviewVisibility = configuration.mediaPreviewVisibility
underlyingThreadsEnabled = configuration.threadsEnabled
}
}
@@ -0,0 +1,55 @@
//
// 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
struct NotificationItemProxyMockConfiguration {
var event: NotificationEvent? = {
// This is likely the most common notification kind
// So it's also the best to use as a default test
let messageType = MessageType.text(content: TextMessageContent(body: "Hello world!", formatted: nil))
let messageLikeContent = MessageLikeEventContent.roomMessage(messageType: messageType, inReplyToEventId: nil)
let event = TimelineEventSDKMock()
event.eventIdUnderlyingReturnValue = UUID().uuidString
event.eventTypeReturnValue = TimelineEventType.messageLike(content: messageLikeContent)
return .timeline(event: event)
}()
var senderID: String = UUID().uuidString
var roomID: String = UUID().uuidString
var receiverID: String = UUID().uuidString
var senderDisplayName: String?
var senderAvatarMediaSource: MediaSourceProxy?
var roomAvatarMediaSource: MediaSourceProxy?
var roomDisplayName: String
var roomJoinedMembers = 2
var isRoomDirect = false
var isRoomPrivate = false
var isNoisy = false
var hasMention = false
var threadRootEventID: String?
}
extension NotificationItemProxyMock {
convenience init(_ configuration: NotificationItemProxyMockConfiguration) {
self.init()
event = configuration.event
underlyingSenderID = configuration.senderID
underlyingRoomID = configuration.roomID
underlyingReceiverID = configuration.receiverID
senderDisplayName = configuration.senderDisplayName
roomAvatarMediaSource = configuration.roomAvatarMediaSource
underlyingRoomDisplayName = configuration.roomDisplayName
underlyingRoomJoinedMembers = configuration.roomJoinedMembers
underlyingIsRoomDirect = configuration.isRoomDirect
underlyingIsRoomPrivate = configuration.isRoomPrivate
underlyingIsNoisy = configuration.isNoisy
underlyingHasMention = configuration.hasMention
threadRootEventID = configuration.threadRootEventID
}
}
@@ -1,118 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector 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 UserNotifications
struct NotificationItemProxy: NotificationItemProxyProtocol {
let notificationItem: NotificationItem
let eventID: String
let receiverID: String
let roomID: String
var event: NotificationEvent? {
notificationItem.event
}
var senderDisplayName: String? {
notificationItem.senderInfo.displayName
}
var senderID: String {
switch notificationItem.event {
case .timeline(let event):
return event.senderId()
case .invite(let senderID):
return senderID
}
}
var roomDisplayName: String {
notificationItem.roomInfo.displayName
}
var isRoomDirect: Bool {
notificationItem.roomInfo.isDirect
}
var isRoomPrivate: Bool {
switch notificationItem.roomInfo.joinRule {
case .invite, .knock, .restricted, .knockRestricted:
true
default:
false
}
}
var roomJoinedMembers: Int {
Int(notificationItem.roomInfo.joinedMembersCount)
}
var isNoisy: Bool {
notificationItem.isNoisy ?? false
}
var hasMention: Bool {
notificationItem.hasMention ?? false
}
var senderAvatarMediaSource: MediaSourceProxy? {
if let senderAvatarURLString = notificationItem.senderInfo.avatarUrl,
let senderAvatarURL = URL(string: senderAvatarURLString) {
return try? MediaSourceProxy(url: senderAvatarURL, mimeType: nil)
}
return nil
}
var roomAvatarMediaSource: MediaSourceProxy? {
if let roomAvatarURLString = notificationItem.roomInfo.avatarUrl,
let roomAvatarURL = URL(string: roomAvatarURLString) {
return try? MediaSourceProxy(url: roomAvatarURL, mimeType: nil)
}
return nil
}
var threadRootEventID: String? {
notificationItem.threadId
}
}
struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
let eventID: String
var event: NotificationEvent? {
nil
}
let roomID: String
let receiverID: String
var senderID: String { "" }
var senderDisplayName: String? { nil }
var roomDisplayName: String { "" }
var isNoisy: Bool { false }
var isRoomDirect: Bool { false }
var isRoomPrivate: Bool { false }
var senderAvatarMediaSource: MediaSourceProxy? { nil }
var roomAvatarMediaSource: MediaSourceProxy? { nil }
var roomJoinedMembers: Int { 0 }
var hasMention: Bool { false }
var threadRootEventID: String? { nil }
}
@@ -1,47 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector 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 UserNotifications
protocol NotificationItemProxyProtocol {
var event: NotificationEvent? { get }
var senderID: String { get }
var roomID: String { get }
var receiverID: String { get }
var senderDisplayName: String? { get }
var senderAvatarMediaSource: MediaSourceProxy? { get }
var roomDisplayName: String { get }
var roomAvatarMediaSource: MediaSourceProxy? { get }
var roomJoinedMembers: Int { get }
var isRoomDirect: Bool { get }
var isRoomPrivate: Bool { get }
var isNoisy: Bool { get }
var hasMention: Bool { get }
var threadRootEventID: String? { get }
}
extension NotificationItemProxyProtocol {
var isDM: Bool {
isRoomDirect && roomJoinedMembers <= 2
}
}
+7
View File
@@ -261,3 +261,10 @@ targets:
- path: ../SupportingFiles
- path: ../../Tools/Scripts/Templates/SimpleScreenExample/ElementX
- path: ../../DevelopmentAssets/Media
# This is required for testing purposes... The problem with unit tests is that if you
# try to `@testable import NSE` you can only get the definition of files, but due to a
# limitation in how they work, they can only compile files that are part of the main test
# target, so linking will fail. The solution is to just have the main app to reference the
# extension code, and test it directly.
- path: ../../NSE/Sources