Tests for audio records screen
This commit is contained in:
parent
65de09845e
commit
6ce8c31635
@ -13,6 +13,7 @@ struct AudioRecordView: View {
|
||||
|
||||
let record: AudioRecordViewModel
|
||||
let progress: Double
|
||||
let onPlay: () -> Void
|
||||
|
||||
var body: some View {
|
||||
|
||||
@ -37,7 +38,7 @@ struct AudioRecordView: View {
|
||||
|
||||
var playButton: some View {
|
||||
Button {
|
||||
record.onPlay()
|
||||
onPlay()
|
||||
} label: {
|
||||
Image(systemName: record.isPlaying ? "pause.fill" : "play.fill")
|
||||
.padding()
|
||||
@ -71,7 +72,8 @@ struct AudioRecordView: View {
|
||||
raw: "бла-бла",
|
||||
duration: 145,
|
||||
event: nil
|
||||
), onPlay: {}),
|
||||
progress: 0.5
|
||||
)),
|
||||
progress: 0.5,
|
||||
onPlay: {}
|
||||
)
|
||||
}
|
||||
|
||||
@ -16,10 +16,8 @@ struct AudioRecordViewModel: Identifiable {
|
||||
var duration: String?
|
||||
var number: String?
|
||||
var date: String
|
||||
var onPlay: () -> Void
|
||||
var url: URL?
|
||||
|
||||
init(dto: AudioRecordDto, isPlaying: Bool = false, onPlay: @escaping () -> Void) {
|
||||
init(dto: AudioRecordDto, isPlaying: Bool = false) {
|
||||
|
||||
self.id = dto.id
|
||||
self.duration = Formatters.time.string(from: dto.duration)
|
||||
@ -28,7 +26,5 @@ struct AudioRecordViewModel: Identifiable {
|
||||
from: Date(timeIntervalSince1970: dto.addedDate)
|
||||
)
|
||||
self.isPlaying = isPlaying
|
||||
self.onPlay = onPlay
|
||||
self.url = try? FileManager.default.url(for: dto.path, in: Constants.audioRecordsFolder)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,14 +18,28 @@ struct RecordsScreen: View {
|
||||
@State var selectedRecordId: String = ""
|
||||
|
||||
var body: some View {
|
||||
List(viewModel.recordModels) { record in
|
||||
AudioRecordView(record: record, progress: viewModel.progress)
|
||||
List {
|
||||
ForEach(viewModel.recordSections) { section in
|
||||
Section(header: Text(section.header)) {
|
||||
ForEach(section.elements) { model in
|
||||
AudioRecordView(
|
||||
record: .init(
|
||||
dto: model,
|
||||
isPlaying: model.id == viewModel.playingRecord?.id
|
||||
),
|
||||
progress: viewModel.progress
|
||||
) {
|
||||
viewModel.onPlayTapped(record: model)
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.swipeActions(allowsFullSwipe: false) {
|
||||
makeActions(for: record)
|
||||
makeActions(for: model)
|
||||
}
|
||||
.contextMenu {
|
||||
makeActions(for: record, useLabels: true)
|
||||
makeActions(for: model, useLabels: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.inset)
|
||||
@ -61,7 +75,7 @@ struct RecordsScreen: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeActions(for record: AudioRecordViewModel, useLabels: Bool = false) -> some View {
|
||||
func makeActions(for record: AudioRecordDto, useLabels: Bool = false) -> some View {
|
||||
|
||||
if useLabels {
|
||||
makeMenuActions(for: record)
|
||||
@ -87,22 +101,22 @@ struct RecordsScreen: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeMenuActions(for record: AudioRecordViewModel) -> some View {
|
||||
func makeMenuActions(for record: AudioRecordDto) -> some View {
|
||||
|
||||
Button {
|
||||
viewModel.showRawRecognizedText(id: record.id)
|
||||
viewModel.showRawRecognizedText(record)
|
||||
} label: {
|
||||
Label("Show recognized text", systemImage: "textformat")
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.showOnMap(id: record.id)
|
||||
viewModel.showOnMap(record)
|
||||
} label: {
|
||||
Label("Show on map", systemImage: "map")
|
||||
}
|
||||
|
||||
Button {
|
||||
viewModel.check(id: record.id)
|
||||
viewModel.check(record)
|
||||
} label: {
|
||||
Label("Check", systemImage: "eye")
|
||||
}
|
||||
|
||||
@ -25,15 +25,9 @@ final class RecordsViewModel: ACHudContainer {
|
||||
var playingRecord: AudioRecordDto?
|
||||
var progress: Double = 0
|
||||
|
||||
var recordModels: [AudioRecordViewModel] {
|
||||
return records.map { record in
|
||||
AudioRecordViewModel(
|
||||
dto: record,
|
||||
isPlaying: record.id == playingRecord?.id
|
||||
) { [weak self] in
|
||||
self?.onPlayTapped(record: record)
|
||||
}
|
||||
}
|
||||
var recordSections: [DateSection<AudioRecordDto>] {
|
||||
|
||||
records.groupedByDate(type: .updatedDate)
|
||||
}
|
||||
|
||||
init(recordService: VehicleRecordServiceProtocol,
|
||||
@ -78,13 +72,11 @@ final class RecordsViewModel: ACHudContainer {
|
||||
do {
|
||||
try recordPlayer.play(record: record, onStop: { [weak self] dto, error in
|
||||
self?.playingRecord = nil
|
||||
//self?.progress = 0
|
||||
if let error {
|
||||
self?.hud = .error(error)
|
||||
}
|
||||
}, onProgress: { [weak self] progress in
|
||||
self?.progress = progress
|
||||
print("==== progress: \(progress)")
|
||||
})
|
||||
|
||||
if playingRecord != record {
|
||||
@ -115,28 +107,21 @@ final class RecordsViewModel: ACHudContainer {
|
||||
}
|
||||
}
|
||||
|
||||
func showRawRecognizedText(id: String) {
|
||||
guard let record = records.first(where: { $0.id == id }) else {
|
||||
return
|
||||
}
|
||||
func showRawRecognizedText(_ record: AudioRecordDto) {
|
||||
|
||||
hud = .message(record.rawText)
|
||||
}
|
||||
|
||||
func showOnMap(id: String) {
|
||||
guard let record = records.first(where: { $0.id == id }),
|
||||
let event = record.event
|
||||
else {
|
||||
func showOnMap(_ record: AudioRecordDto) {
|
||||
guard let event = record.event else {
|
||||
return
|
||||
}
|
||||
|
||||
coordinator?.showOnMap(event: event)
|
||||
}
|
||||
|
||||
func check(id: String) {
|
||||
guard let record = records.first(where: { $0.id == id }),
|
||||
let number = record.number
|
||||
else {
|
||||
func check(_ record: AudioRecordDto) {
|
||||
guard let number = record.number else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,10 @@ public struct AudioRecordDto: Decodable, Sendable, Equatable {
|
||||
public var duration: TimeInterval = 0
|
||||
public var event: VehicleEventDto?
|
||||
|
||||
public var url: URL? {
|
||||
try? FileManager.default.url(for: path, in: Constants.audioRecordsFolder)
|
||||
}
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
number: String?,
|
||||
|
||||
@ -6,8 +6,9 @@
|
||||
// Copyright © 2025 Selim Mustafaev. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Mockable
|
||||
|
||||
@Mockable
|
||||
public protocol RecordPlayerServiceProtocol {
|
||||
|
||||
var currentPlayingId: String? { get }
|
||||
|
||||
@ -6,8 +6,9 @@
|
||||
// Copyright © 2025 Selim Mustafaev. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Mockable
|
||||
|
||||
@Mockable
|
||||
public protocol VehicleRecordServiceProtocol: Sendable {
|
||||
|
||||
func requestPermissionsIfNeeded() async
|
||||
|
||||
340
AutoCatTests/AudioRecordsTests.swift
Normal file
340
AutoCatTests/AudioRecordsTests.swift
Normal file
@ -0,0 +1,340 @@
|
||||
//
|
||||
// AudioRecordsTests.swift
|
||||
// AutoCatTests
|
||||
//
|
||||
// Created by Selim Mustafaev on 05.04.2025.
|
||||
// Copyright © 2025 Selim Mustafaev. All rights reserved.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Mockable
|
||||
import AutoCatCore
|
||||
@testable import AutoCat
|
||||
|
||||
@MainActor
|
||||
struct AudioRecordsTests {
|
||||
|
||||
let recordServiceMock = MockVehicleRecordServiceProtocol()
|
||||
let storageServiceMock = MockStorageServiceProtocol()
|
||||
let recordPlayerMock = MockRecordPlayerServiceProtocol()
|
||||
|
||||
let viewModel: RecordsViewModel
|
||||
|
||||
init() async throws {
|
||||
|
||||
viewModel = .init(
|
||||
recordService: recordServiceMock,
|
||||
storageService: storageServiceMock,
|
||||
recordPlayer: recordPlayerMock
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Initial load")
|
||||
func initialLoad() async throws {
|
||||
|
||||
given(storageServiceMock)
|
||||
.loadRecords()
|
||||
.willReturn([AudioRecordDto.default])
|
||||
|
||||
given(recordServiceMock)
|
||||
.requestPermissionsIfNeeded()
|
||||
.willReturn()
|
||||
|
||||
await viewModel.onAppear()
|
||||
|
||||
verify(storageServiceMock)
|
||||
.loadRecords()
|
||||
.called(.once)
|
||||
|
||||
verify(recordServiceMock)
|
||||
.requestPermissionsIfNeeded()
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records.count == 1)
|
||||
#expect(viewModel.hud == nil)
|
||||
}
|
||||
|
||||
@Test("Initial load error")
|
||||
func initialLoadError() async throws {
|
||||
|
||||
given(storageServiceMock)
|
||||
.loadRecords()
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
given(recordServiceMock)
|
||||
.requestPermissionsIfNeeded()
|
||||
.willReturn()
|
||||
|
||||
await viewModel.onAppear()
|
||||
|
||||
verify(storageServiceMock)
|
||||
.loadRecords()
|
||||
.called(.once)
|
||||
|
||||
verify(recordServiceMock)
|
||||
.requestPermissionsIfNeeded()
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records.isEmpty)
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Start recording")
|
||||
func startRecording() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.startRecording()
|
||||
.willReturn()
|
||||
|
||||
await viewModel.startRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.startRecording()
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.showRecordingAlert == true)
|
||||
#expect(viewModel.hud == nil)
|
||||
}
|
||||
|
||||
@Test("Start recording error")
|
||||
func startRecordingError() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.startRecording()
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
await viewModel.startRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.startRecording()
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.showRecordingAlert == false)
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Stop recording")
|
||||
func stopRecording() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.stopRecording()
|
||||
.willReturn(.default)
|
||||
|
||||
given(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.willReturn()
|
||||
|
||||
given(storageServiceMock)
|
||||
.loadRecords()
|
||||
.willReturn([.default])
|
||||
|
||||
await viewModel.stopRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.stopRecording()
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.loadRecords()
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records.count == 1)
|
||||
#expect(viewModel.hud == nil)
|
||||
}
|
||||
|
||||
@Test("Stop recording (recording error)")
|
||||
func stopRecordingError() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.stopRecording()
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
await viewModel.stopRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.stopRecording()
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.called(.never)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.loadRecords()
|
||||
.called(.never)
|
||||
|
||||
#expect(viewModel.records.isEmpty)
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Stop recording (add error)")
|
||||
func stopRecordingAddError() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.stopRecording()
|
||||
.willReturn(.default)
|
||||
|
||||
given(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
await viewModel.stopRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.stopRecording()
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.loadRecords()
|
||||
.called(.never)
|
||||
|
||||
#expect(viewModel.records.isEmpty)
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Stop recording (load error)")
|
||||
func stopRecordingLoadError() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.stopRecording()
|
||||
.willReturn(.default)
|
||||
|
||||
given(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.willReturn()
|
||||
|
||||
given(storageServiceMock)
|
||||
.loadRecords()
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
await viewModel.stopRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.stopRecording()
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.add(record: .any)
|
||||
.called(.once)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.loadRecords()
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records.isEmpty)
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Cancel recording")
|
||||
func cancelRecording() async throws {
|
||||
|
||||
given(recordServiceMock)
|
||||
.cancelRecording()
|
||||
.willReturn()
|
||||
|
||||
await viewModel.cancelRecording()
|
||||
|
||||
verify(recordServiceMock)
|
||||
.cancelRecording()
|
||||
.called(.once)
|
||||
}
|
||||
|
||||
@Test("Delete record")
|
||||
func deleteRecord() async throws {
|
||||
|
||||
viewModel.records = [.default]
|
||||
|
||||
given(storageServiceMock)
|
||||
.deleteRecord(id: .any)
|
||||
.willReturn()
|
||||
|
||||
await viewModel.deleteRecord(id: AudioRecordDto.default.id)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.deleteRecord(id: .any)
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records.isEmpty)
|
||||
#expect(viewModel.hud == nil)
|
||||
}
|
||||
|
||||
@Test("Delete record (error)")
|
||||
func deleteRecordError() async throws {
|
||||
|
||||
viewModel.records = [.default]
|
||||
|
||||
given(storageServiceMock)
|
||||
.deleteRecord(id: .any)
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
await viewModel.deleteRecord(id: AudioRecordDto.default.id)
|
||||
|
||||
verify(storageServiceMock)
|
||||
.deleteRecord(id: .any)
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records.count == 1)
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Edit record")
|
||||
func editRecord() async throws {
|
||||
|
||||
var editedRecord: AudioRecordDto = .default
|
||||
editedRecord.number = "123"
|
||||
|
||||
viewModel.records = [.default]
|
||||
|
||||
given(storageServiceMock)
|
||||
.updateRecord(id: .any, number: .any)
|
||||
.willReturn(editedRecord)
|
||||
|
||||
await viewModel.editRecord(id: AudioRecordDto.default.id, number: "123")
|
||||
|
||||
verify(storageServiceMock)
|
||||
.updateRecord(id: .any, number: .any)
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records == [editedRecord])
|
||||
#expect(viewModel.hud == nil)
|
||||
}
|
||||
|
||||
@Test("Edit record (error)")
|
||||
func editRecordError() async throws {
|
||||
|
||||
let record: AudioRecordDto = .default
|
||||
viewModel.records = [record]
|
||||
|
||||
given(storageServiceMock)
|
||||
.updateRecord(id: .any, number: .any)
|
||||
.willThrow(TestError.generic)
|
||||
|
||||
await viewModel.editRecord(id: AudioRecordDto.default.id, number: "123")
|
||||
|
||||
verify(storageServiceMock)
|
||||
.updateRecord(id: .any, number: .any)
|
||||
.called(.once)
|
||||
|
||||
#expect(viewModel.records == [record])
|
||||
#expect(viewModel.hud == .error(TestError.generic))
|
||||
}
|
||||
|
||||
@Test("Show recognized text")
|
||||
func showRecognizedText() async throws {
|
||||
|
||||
var record: AudioRecordDto = .default
|
||||
record.rawText = "Test text"
|
||||
|
||||
viewModel.showRawRecognizedText(record)
|
||||
|
||||
#expect(viewModel.hud == .message("Test text"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user