Add unit tests for voice messages (#1869)

This commit is contained in:
Nicolas Mauri
2023-10-10 08:03:40 +02:00
committed by GitHub
parent f7bf4b6e20
commit cb9d35948e
7 changed files with 438 additions and 0 deletions
+4
View File
@@ -650,6 +650,7 @@
C0DC02E2B91DC76A4D1A0E7F /* OnboardingScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3450F4C32D73532DBBC1A2 /* OnboardingScreenBackgroundImage.swift */; };
C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */; };
C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; };
C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */; };
C19085A284D54A166A64A86C /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA88C615D8BFCCEF0D2FEAC9 /* AudioPlayerState.swift */; };
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; };
C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */; };
@@ -1315,6 +1316,7 @@
8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = "<group>"; };
8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreen.swift; sourceTree = "<group>"; };
889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerProtocol.swift; sourceTree = "<group>"; };
89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerStateTests.swift; sourceTree = "<group>"; };
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = "<group>"; };
8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -2760,6 +2762,7 @@
893777A4997BBDB68079D4F5 /* ArrayTests.swift */,
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */,
37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */,
89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */,
6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */,
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */,
7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */,
@@ -4550,6 +4553,7 @@
3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */,
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */,
5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */,
C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */,
0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */,
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */,
@@ -115,6 +115,94 @@ class AnalyticsClientMock: AnalyticsClientProtocol {
updateUserPropertiesClosure?(userProperties)
}
}
class AudioPlayerMock: AudioPlayerProtocol {
var actions: AnyPublisher<AudioPlayerAction, Never> {
get { return underlyingActions }
set(value) { underlyingActions = value }
}
var underlyingActions: AnyPublisher<AudioPlayerAction, Never>!
var mediaSource: MediaSourceProxy?
var currentTime: TimeInterval {
get { return underlyingCurrentTime }
set(value) { underlyingCurrentTime = value }
}
var underlyingCurrentTime: TimeInterval!
var url: URL?
var state: MediaPlayerState {
get { return underlyingState }
set(value) { underlyingState = value }
}
var underlyingState: MediaPlayerState!
//MARK: - load
var loadMediaSourceUsingCallsCount = 0
var loadMediaSourceUsingCalled: Bool {
return loadMediaSourceUsingCallsCount > 0
}
var loadMediaSourceUsingReceivedArguments: (mediaSource: MediaSourceProxy, url: URL)?
var loadMediaSourceUsingReceivedInvocations: [(mediaSource: MediaSourceProxy, url: URL)] = []
var loadMediaSourceUsingClosure: ((MediaSourceProxy, URL) -> Void)?
func load(mediaSource: MediaSourceProxy, using url: URL) {
loadMediaSourceUsingCallsCount += 1
loadMediaSourceUsingReceivedArguments = (mediaSource: mediaSource, url: url)
loadMediaSourceUsingReceivedInvocations.append((mediaSource: mediaSource, url: url))
loadMediaSourceUsingClosure?(mediaSource, url)
}
//MARK: - play
var playCallsCount = 0
var playCalled: Bool {
return playCallsCount > 0
}
var playClosure: (() -> Void)?
func play() {
playCallsCount += 1
playClosure?()
}
//MARK: - pause
var pauseCallsCount = 0
var pauseCalled: Bool {
return pauseCallsCount > 0
}
var pauseClosure: (() -> Void)?
func pause() {
pauseCallsCount += 1
pauseClosure?()
}
//MARK: - stop
var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?
func stop() {
stopCallsCount += 1
stopClosure?()
}
//MARK: - seek
var seekToCallsCount = 0
var seekToCalled: Bool {
return seekToCallsCount > 0
}
var seekToReceivedProgress: Double?
var seekToReceivedInvocations: [Double] = []
var seekToClosure: ((Double) async -> Void)?
func seek(to progress: Double) async {
seekToCallsCount += 1
seekToReceivedProgress = progress
seekToReceivedInvocations.append(progress)
await seekToClosure?(progress)
}
}
class BugReportServiceMock: BugReportServiceProtocol {
var isRunning: Bool {
get { return underlyingIsRunning }
@@ -226,6 +314,89 @@ class CompletionSuggestionServiceMock: CompletionSuggestionServiceProtocol {
setSuggestionTriggerClosure?(suggestionTrigger)
}
}
class MediaPlayerMock: MediaPlayerProtocol {
var mediaSource: MediaSourceProxy?
var currentTime: TimeInterval {
get { return underlyingCurrentTime }
set(value) { underlyingCurrentTime = value }
}
var underlyingCurrentTime: TimeInterval!
var url: URL?
var state: MediaPlayerState {
get { return underlyingState }
set(value) { underlyingState = value }
}
var underlyingState: MediaPlayerState!
//MARK: - load
var loadMediaSourceUsingCallsCount = 0
var loadMediaSourceUsingCalled: Bool {
return loadMediaSourceUsingCallsCount > 0
}
var loadMediaSourceUsingReceivedArguments: (mediaSource: MediaSourceProxy, url: URL)?
var loadMediaSourceUsingReceivedInvocations: [(mediaSource: MediaSourceProxy, url: URL)] = []
var loadMediaSourceUsingClosure: ((MediaSourceProxy, URL) -> Void)?
func load(mediaSource: MediaSourceProxy, using url: URL) {
loadMediaSourceUsingCallsCount += 1
loadMediaSourceUsingReceivedArguments = (mediaSource: mediaSource, url: url)
loadMediaSourceUsingReceivedInvocations.append((mediaSource: mediaSource, url: url))
loadMediaSourceUsingClosure?(mediaSource, url)
}
//MARK: - play
var playCallsCount = 0
var playCalled: Bool {
return playCallsCount > 0
}
var playClosure: (() -> Void)?
func play() {
playCallsCount += 1
playClosure?()
}
//MARK: - pause
var pauseCallsCount = 0
var pauseCalled: Bool {
return pauseCallsCount > 0
}
var pauseClosure: (() -> Void)?
func pause() {
pauseCallsCount += 1
pauseClosure?()
}
//MARK: - stop
var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?
func stop() {
stopCallsCount += 1
stopClosure?()
}
//MARK: - seek
var seekToCallsCount = 0
var seekToCalled: Bool {
return seekToCallsCount > 0
}
var seekToReceivedProgress: Double?
var seekToReceivedInvocations: [Double] = []
var seekToClosure: ((Double) async -> Void)?
func seek(to progress: Double) async {
seekToCallsCount += 1
seekToReceivedProgress = progress
seekToReceivedInvocations.append(progress)
await seekToClosure?(progress)
}
}
class NotificationCenterMock: NotificationCenterProtocol {
//MARK: - post
@@ -1914,4 +2085,32 @@ class UserNotificationCenterMock: UserNotificationCenterProtocol {
}
}
}
class VoiceMessageMediaManagerMock: VoiceMessageMediaManagerProtocol {
//MARK: - loadVoiceMessageFromSource
var loadVoiceMessageFromSourceBodyThrowableError: Error?
var loadVoiceMessageFromSourceBodyCallsCount = 0
var loadVoiceMessageFromSourceBodyCalled: Bool {
return loadVoiceMessageFromSourceBodyCallsCount > 0
}
var loadVoiceMessageFromSourceBodyReceivedArguments: (source: MediaSourceProxy, body: String?)?
var loadVoiceMessageFromSourceBodyReceivedInvocations: [(source: MediaSourceProxy, body: String?)] = []
var loadVoiceMessageFromSourceBodyReturnValue: URL!
var loadVoiceMessageFromSourceBodyClosure: ((MediaSourceProxy, String?) async throws -> URL)?
func loadVoiceMessageFromSource(_ source: MediaSourceProxy, body: String?) async throws -> URL {
if let error = loadVoiceMessageFromSourceBodyThrowableError {
throw error
}
loadVoiceMessageFromSourceBodyCallsCount += 1
loadVoiceMessageFromSourceBodyReceivedArguments = (source: source, body: body)
loadVoiceMessageFromSourceBodyReceivedInvocations.append((source: source, body: body))
if let loadVoiceMessageFromSourceBodyClosure = loadVoiceMessageFromSourceBodyClosure {
return try await loadVoiceMessageFromSourceBodyClosure(source, body)
} else {
return loadVoiceMessageFromSourceBodyReturnValue
}
}
}
// swiftlint:enable all
@@ -30,3 +30,6 @@ enum AudioPlayerAction {
protocol AudioPlayerProtocol: MediaPlayerProtocol {
var actions: AnyPublisher<AudioPlayerAction, Never> { get }
}
// sourcery: AutoMockable
extension AudioPlayerProtocol { }
@@ -40,6 +40,10 @@ class AudioPlayerState: ObservableObject {
var isAttached: Bool {
audioPlayer != nil
}
var isPublishingProgress: Bool {
cancellableTimer != nil
}
init(duration: Double, waveform: Waveform? = nil, progress: Double = 0.0) {
self.duration = duration
@@ -133,6 +137,7 @@ class AudioPlayerState: ObservableObject {
private func stopPublishProgress() {
cancellableTimer?.cancel()
cancellableTimer = nil
}
private func restoreAudioPlayerState(audioPlayer: AudioPlayerProtocol) async {
@@ -37,3 +37,6 @@ protocol MediaPlayerProtocol {
func stop()
func seek(to progress: Double) async
}
// sourcery: AutoMockable
extension MediaPlayerProtocol { }
@@ -19,3 +19,6 @@ import Foundation
protocol VoiceMessageMediaManagerProtocol {
func loadVoiceMessageFromSource(_ source: MediaSourceProxy, body: String?) async throws -> URL
}
// sourcery: AutoMockable
extension VoiceMessageMediaManagerProtocol { }
@@ -0,0 +1,221 @@
//
// Copyright 2023 New Vector Ltd
//
// 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
//
// http://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.
//
import Combine
@testable import ElementX
import Foundation
import XCTest
@MainActor
class AudioPlayerStateTests: XCTestCase {
private var audioPlayerState: AudioPlayerState!
private var audioPlayerMock: AudioPlayerMock!
private var audioPlayerActionsSubject: PassthroughSubject<AudioPlayerAction, Never>!
private var audioPlayerActions: AnyPublisher<AudioPlayerAction, Never> {
audioPlayerActionsSubject.eraseToAnyPublisher()
}
private var audioPlayerSeekCallsSubject: PassthroughSubject<Double, Never>!
private var audioPlayerSeekCalls: AnyPublisher<Double, Never> {
audioPlayerSeekCallsSubject.eraseToAnyPublisher()
}
private func buildAudioPlayerMock() -> AudioPlayerMock {
let audioPlayerMock = AudioPlayerMock()
audioPlayerMock.underlyingActions = audioPlayerActions
audioPlayerMock.currentTime = 0.0
audioPlayerMock.seekToClosure = { [audioPlayerSeekCallsSubject] progress in
audioPlayerSeekCallsSubject?.send(progress)
}
return audioPlayerMock
}
override func setUp() async throws {
audioPlayerActionsSubject = .init()
audioPlayerSeekCallsSubject = .init()
audioPlayerState = AudioPlayerState(duration: 10.0)
audioPlayerMock = buildAudioPlayerMock()
}
func testAttach() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
XCTAssert(audioPlayerState.isAttached)
XCTAssertEqual(audioPlayerState.playbackState, .loading)
}
func testDetach() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
audioPlayerState.detachAudioPlayer()
XCTAssert(audioPlayerMock.stopCalled)
XCTAssertFalse(audioPlayerState.isAttached)
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
}
func testReportError() async throws {
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
audioPlayerState.reportError(AudioPlayerError.genericError)
XCTAssertEqual(audioPlayerState.playbackState, .error)
}
func testUpdateProgress() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
// If we try to set a negative progress, the new progress must be 0.0
do {
await audioPlayerState.updateState(progress: -5.0)
XCTAssertEqual(audioPlayerState.progress, 0.0)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.0)
}
// If we try to set a progress > 1.0, the new progress must be 1.0
do {
await audioPlayerState.updateState(progress: 1.5)
XCTAssertEqual(audioPlayerState.progress, 1.0)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 1.0)
}
do {
await audioPlayerState.updateState(progress: 0.4)
XCTAssertEqual(audioPlayerState.progress, 0.4)
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4)
}
}
func testHandlingAudioPlayerActionDidStartLoading() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerActions) { action in
switch action {
case .didStartLoading:
return true
default:
return false
}
}
audioPlayerActionsSubject.send(.didStartLoading)
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .loading)
}
func testHandlingAudioPlayerActionDidFinishLoading() async throws {
let originalStateProgress = 0.4
await audioPlayerState.updateState(progress: originalStateProgress)
audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerActions) { action in
switch action {
case .didFinishLoading:
return true
default:
return false
}
}
// The progress should be restored
let deferedProgress = deferFulfillment(audioPlayerSeekCalls) { progress in
progress == originalStateProgress
}
audioPlayerActionsSubject.send(.didFinishLoading)
try await deferred.fulfill()
try await deferedProgress.fulfill()
// The state is expected to be .readyToPlay
XCTAssertEqual(audioPlayerState.playbackState, .readyToPlay)
}
func testHandlingAudioPlayerActionDidStartPlaying() async throws {
audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerActions) { action in
switch action {
case .didStartPlaying:
return true
default:
return false
}
}
audioPlayerActionsSubject.send(.didStartPlaying)
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .playing)
XCTAssert(audioPlayerState.isPublishingProgress)
}
func testHandlingAudioPlayerActionDidPausePlaying() async throws {
await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerActions) { action in
switch action {
case .didPausePlaying:
return true
default:
return false
}
}
audioPlayerActionsSubject.send(.didPausePlaying)
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
XCTAssertEqual(audioPlayerState.progress, 0.4)
XCTAssertFalse(audioPlayerState.isPublishingProgress)
}
func testHandlingAudioPlayerActionsidStopPlaying() async throws {
await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerActions) { action in
switch action {
case .didStopPlaying:
return true
default:
return false
}
}
audioPlayerActionsSubject.send(.didStopPlaying)
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
XCTAssertEqual(audioPlayerState.progress, 0.4)
XCTAssertFalse(audioPlayerState.isPublishingProgress)
}
func testAudioPlayerActionsDidFinishPlaying() async throws {
await audioPlayerState.updateState(progress: 0.4)
audioPlayerState.attachAudioPlayer(audioPlayerMock)
let deferred = deferFulfillment(audioPlayerActions) { action in
switch action {
case .didFinishPlaying:
return true
default:
return false
}
}
audioPlayerActionsSubject.send(.didFinishPlaying)
try await deferred.fulfill()
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
// Progress should be reset to 0
XCTAssertEqual(audioPlayerState.progress, 0.0)
XCTAssertFalse(audioPlayerState.isPublishingProgress)
}
}