| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- import SwiftData
- import XCTest
- @testable import MixBoard
- // MARK: - Test Helpers
- /// Creates an in-memory ModelContainer for testing SwiftData operations.
- func makeInMemoryContainer() throws -> ModelContainer {
- let config = ModelConfiguration(isStoredInMemoryOnly: true)
- return try ModelContainer(
- for: Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self,
- configurations: config
- )
- }
- // MARK: - PlaylistViewModelTests
- @MainActor
- final class PlaylistViewModelTests: XCTestCase {
- private var container: ModelContainer!
- private var context: ModelContext!
- private var vm: PlaylistViewModel!
- override func setUp() async throws {
- container = try makeInMemoryContainer()
- context = ModelContext(container)
- vm = PlaylistViewModel()
- }
- override func tearDown() {
- // Clean up UserDefaults keys used by mix targets
- for slot in 0..<3 {
- UserDefaults.standard.removeObject(forKey: "mixTarget\(slot)ID")
- }
- }
- // MARK: - Initial State
- func testInitialState() {
- XCTAssertNil(vm.selectedPlaylist)
- XCTAssertFalse(vm.showExportSheet)
- XCTAssertNil(vm.exportError)
- XCTAssertNil(vm.statusMessage)
- XCTAssertEqual(vm.defaultCrossfadeDuration, 2.0)
- }
- func testInitialMixTargetsNil() {
- XCTAssertEqual(vm.mixTargets.count, 3)
- XCTAssertNil(vm.mixTargets[0])
- XCTAssertNil(vm.mixTargets[1])
- XCTAssertNil(vm.mixTargets[2])
- }
- // MARK: - Mix Targets
- func testSetMixTargetValidSlot() {
- let playlist = Playlist(name: "Mix A")
- context.insert(playlist)
- try? context.save()
- vm.setMixTarget(0, playlist: playlist)
- XCTAssertEqual(vm.mixTargets[0]?.name, "Mix A")
- XCTAssertEqual(UserDefaults.standard.string(forKey: "mixTarget0ID"), playlist.id.uuidString)
- }
- func testSetMixTargetAllSlots() {
- let p0 = Playlist(name: "Red Mix")
- let p1 = Playlist(name: "Blue Mix")
- let p2 = Playlist(name: "Gold Mix")
- context.insert(p0); context.insert(p1); context.insert(p2)
- try? context.save()
- vm.setMixTarget(0, playlist: p0)
- vm.setMixTarget(1, playlist: p1)
- vm.setMixTarget(2, playlist: p2)
- XCTAssertEqual(vm.mixTargets[0]?.name, "Red Mix")
- XCTAssertEqual(vm.mixTargets[1]?.name, "Blue Mix")
- XCTAssertEqual(vm.mixTargets[2]?.name, "Gold Mix")
- }
- func testSetMixTargetInvalidSlot() {
- let playlist = Playlist(name: "Test")
- vm.setMixTarget(-1, playlist: playlist)
- vm.setMixTarget(3, playlist: playlist)
- // Should not crash, and mixTargets should remain unchanged
- XCTAssertNil(vm.mixTargets[0])
- XCTAssertNil(vm.mixTargets[1])
- XCTAssertNil(vm.mixTargets[2])
- }
- func testSetMixTargetNilClearsUserDefaults() {
- let playlist = Playlist(name: "Target")
- context.insert(playlist)
- try? context.save()
- vm.setMixTarget(0, playlist: playlist)
- XCTAssertNotNil(UserDefaults.standard.string(forKey: "mixTarget0ID"))
- vm.setMixTarget(0, playlist: nil)
- XCTAssertNil(vm.mixTargets[0])
- XCTAssertNil(UserDefaults.standard.string(forKey: "mixTarget0ID"))
- }
- func testMixTargetNameWithPlaylist() {
- let playlist = Playlist(name: "Friday Night")
- vm.setMixTarget(1, playlist: playlist)
- XCTAssertEqual(vm.mixTargetName(1), "Friday Night")
- }
- func testMixTargetNameWithoutPlaylist() {
- XCTAssertEqual(vm.mixTargetName(0), "Mix 1")
- XCTAssertEqual(vm.mixTargetName(1), "Mix 2")
- XCTAssertEqual(vm.mixTargetName(2), "Mix 3")
- }
- func testMixTargetNameInvalidSlot() {
- XCTAssertEqual(vm.mixTargetName(5), "Mix 6")
- }
- func testRestoreTargetPlaylist() {
- let playlist = Playlist(name: "Saved Target")
- context.insert(playlist)
- try? context.save()
- // Simulate persisted state
- UserDefaults.standard.set(playlist.id.uuidString, forKey: "mixTarget0ID")
- vm.restoreTargetPlaylist(from: [playlist])
- XCTAssertEqual(vm.mixTargets[0]?.name, "Saved Target")
- }
- func testRestoreTargetPlaylistNoMatch() {
- UserDefaults.standard.set(UUID().uuidString, forKey: "mixTarget0ID")
- vm.restoreTargetPlaylist(from: [])
- XCTAssertNil(vm.mixTargets[0])
- }
- // MARK: - Legacy targetPlaylist
- func testLegacyTargetPlaylist() {
- let playlist = Playlist(name: "Legacy")
- vm.targetPlaylist = playlist
- XCTAssertEqual(vm.mixTargets[0]?.name, "Legacy")
- XCTAssertEqual(vm.targetPlaylist?.name, "Legacy")
- }
- // MARK: - Playlist CRUD
- func testCreatePlaylist() {
- let playlist = vm.createPlaylist(name: "New Mix", context: context)
- XCTAssertEqual(playlist.name, "New Mix")
- let descriptor = FetchDescriptor<Playlist>()
- let fetched = try? context.fetch(descriptor)
- XCTAssertEqual(fetched?.count, 1)
- XCTAssertEqual(fetched?.first?.name, "New Mix")
- }
- func testDeletePlaylist() {
- let playlist = vm.createPlaylist(name: "To Delete", context: context)
- vm.selectedPlaylist = playlist
- vm.deletePlaylist(playlist, context: context)
- let descriptor = FetchDescriptor<Playlist>()
- let fetched = try? context.fetch(descriptor)
- XCTAssertEqual(fetched?.count, 0)
- XCTAssertNil(vm.selectedPlaylist)
- }
- func testDeletePlaylistDoesNotClearOtherSelection() {
- let p1 = vm.createPlaylist(name: "Keep", context: context)
- let p2 = vm.createPlaylist(name: "Delete", context: context)
- vm.selectedPlaylist = p1
- vm.deletePlaylist(p2, context: context)
- XCTAssertEqual(vm.selectedPlaylist?.name, "Keep")
- }
- func testRenamePlaylist() {
- let playlist = vm.createPlaylist(name: "Original", context: context)
- let originalDate = playlist.dateModified
- vm.renamePlaylist(playlist, to: "Renamed", context: context)
- XCTAssertEqual(playlist.name, "Renamed")
- XCTAssertGreaterThanOrEqual(playlist.dateModified, originalDate)
- }
- // MARK: - Track Management
- func testAddTrackToPlaylist() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addTrack(track, to: playlist, context: context)
- XCTAssertEqual(playlist.trackCount, 1)
- XCTAssertEqual(playlist.sortedEntries.first?.crossfadeDuration, 2.0) // default
- }
- func testAddDuplicateTrackShowsWarning() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addTrack(track, to: playlist, context: context)
- vm.addTrack(track, to: playlist, context: context)
- // Second add should be rejected (duplicate)
- XCTAssertEqual(playlist.trackCount, 1)
- XCTAssertNotNil(vm.statusMessage)
- }
- func testAddTracksMultiple() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let t1 = Track(title: "A", filePath: "Music/b.mp3") // intentionally out of alphabetical order
- let t2 = Track(title: "B", filePath: "Music/a.mp3")
- context.insert(t1); context.insert(t2)
- vm.addTracks([t1, t2], to: playlist, context: context)
- XCTAssertEqual(playlist.trackCount, 2)
- // Should be sorted by filePath
- let sorted = playlist.sortedEntries
- XCTAssertEqual(sorted[0].track?.filePath, "Music/a.mp3")
- XCTAssertEqual(sorted[1].track?.filePath, "Music/b.mp3")
- }
- func testAddTracksSkipsDuplicates() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addTrack(track, to: playlist, context: context)
- vm.addTracks([track], to: playlist, context: context)
- XCTAssertEqual(playlist.trackCount, 1)
- }
- func testRemoveEntry() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addTrack(track, to: playlist, context: context)
- let entry = playlist.sortedEntries.first!
- vm.removeEntry(entry, from: playlist, context: context)
- XCTAssertEqual(playlist.trackCount, 0)
- }
- func testMoveEntry() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let t1 = Track(title: "A", filePath: "Music/a.mp3")
- let t2 = Track(title: "B", filePath: "Music/b.mp3")
- let t3 = Track(title: "C", filePath: "Music/c.mp3")
- context.insert(t1); context.insert(t2); context.insert(t3)
- playlist.addTrack(t1); playlist.addTrack(t2); playlist.addTrack(t3)
- vm.moveEntry(in: playlist, from: 2, to: 0, context: context)
- let sorted = playlist.sortedEntries
- XCTAssertEqual(sorted[0].track?.title, "C")
- XCTAssertEqual(sorted[1].track?.title, "A")
- XCTAssertEqual(sorted[2].track?.title, "B")
- }
- // MARK: - Quick Add
- func testQuickAddToMixNoTarget() {
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- let result = vm.quickAddToMix(slot: 0, track: track, context: context)
- XCTAssertFalse(result)
- XCTAssertNotNil(vm.statusMessage)
- }
- func testQuickAddToMixSuccess() {
- let playlist = vm.createPlaylist(name: "Target", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.setMixTarget(0, playlist: playlist)
- let result = vm.quickAddToMix(slot: 0, track: track, context: context)
- XCTAssertTrue(result)
- XCTAssertEqual(playlist.trackCount, 1)
- }
- func testQuickAddToTargetLegacy() {
- let playlist = vm.createPlaylist(name: "Target", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.targetPlaylist = playlist
- let result = vm.quickAddToTarget(track: track, context: context)
- XCTAssertTrue(result)
- XCTAssertEqual(playlist.trackCount, 1)
- }
- func testQuickAddDuplicate() {
- let playlist = vm.createPlaylist(name: "Target", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.setMixTarget(0, playlist: playlist)
- _ = vm.quickAddToMix(slot: 0, track: track, context: context)
- let result2 = vm.quickAddToMix(slot: 0, track: track, context: context)
- XCTAssertFalse(result2)
- XCTAssertEqual(playlist.trackCount, 1)
- }
- // MARK: - Crossfade & Gain
- func testUpdateCrossfade() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addTrack(track, to: playlist, context: context)
- let entry = playlist.sortedEntries.first!
- vm.updateCrossfade(for: entry, duration: 5.0, context: context)
- XCTAssertEqual(entry.crossfadeDuration, 5.0)
- }
- func testUpdateGain() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addTrack(track, to: playlist, context: context)
- let entry = playlist.sortedEntries.first!
- vm.updateGain(for: entry, gain: 3.5, context: context)
- XCTAssertEqual(entry.gainAdjustment, 3.5)
- }
- // MARK: - Cue Points
- func testAddCuePoint() {
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addCuePoint(to: track, at: 30.0, type: .drop, name: "The Drop", context: context)
- XCTAssertEqual(track.cuePoints.count, 1)
- XCTAssertEqual(track.cuePoints.first?.name, "The Drop")
- XCTAssertEqual(track.cuePoints.first?.type, .drop)
- XCTAssertEqual(track.cuePoints.first?.timestamp, 30.0)
- }
- func testRemoveCuePoint() {
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- vm.addCuePoint(to: track, at: 10.0, name: "Marker", context: context)
- XCTAssertEqual(track.cuePoints.count, 1)
- let cuePoint = track.cuePoints.first!
- vm.removeCuePoint(cuePoint, from: track, context: context)
- XCTAssertEqual(track.cuePoints.count, 0)
- }
- // MARK: - Mix Duration
- func testMixDurationEmpty() {
- let playlist = Playlist(name: "Empty")
- XCTAssertEqual(vm.mixDuration(for: playlist), 0)
- }
- func testMixDurationSingleTrack() {
- let playlist = Playlist(name: "Mix")
- let track = Track(title: "Song", filePath: "Music/song.mp3", duration: 200)
- playlist.addTrack(track)
- XCTAssertEqual(vm.mixDuration(for: playlist), 200, accuracy: 0.01)
- }
- func testMixDurationWithCrossfades() {
- let playlist = Playlist(name: "Mix")
- let t1 = Track(title: "A", filePath: "Music/a.mp3", duration: 200)
- let t2 = Track(title: "B", filePath: "Music/b.mp3", duration: 180)
- let t3 = Track(title: "C", filePath: "Music/c.mp3", duration: 240)
- playlist.addTrack(t1, crossfadeDuration: 0)
- playlist.addTrack(t2, crossfadeDuration: 5)
- playlist.addTrack(t3, crossfadeDuration: 3)
- // total = 200 + 180 + 240 - 5 - 3 = 612
- XCTAssertEqual(vm.mixDuration(for: playlist), 612, accuracy: 0.01)
- }
- // MARK: - Show Status
- func testShowStatus() {
- vm.showStatus("Test message")
- XCTAssertEqual(vm.statusMessage, "Test message")
- }
- func testShowStatusNilInitially() {
- XCTAssertNil(vm.statusMessage)
- }
- // MARK: - isDuplicate
- func testIsDuplicateWithContext() {
- let playlist = vm.createPlaylist(name: "Mix", context: context)
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- context.insert(track)
- XCTAssertFalse(vm.isDuplicate(track: track, in: playlist, context: context))
- vm.addTrack(track, to: playlist, context: context)
- XCTAssertTrue(vm.isDuplicate(track: track, in: playlist, context: context))
- }
- func testIsDuplicateWithoutContext() {
- let playlist = Playlist(name: "Mix")
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- XCTAssertFalse(vm.isDuplicate(track: track, in: playlist))
- playlist.addTrack(track)
- XCTAssertTrue(vm.isDuplicate(track: track, in: playlist))
- }
- }
|