| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- import XCTest
- import SwiftUI
- @testable import MixBoard
- // MARK: - Model Tests
- final class ModelTests: XCTestCase {
- func testTrackInit() {
- let track = Track(
- title: "Test Song",
- artist: "Test Artist",
- filePath: "Music/test.mp3",
- fileName: "test.mp3",
- duration: 180
- )
- XCTAssertEqual(track.title, "Test Song")
- XCTAssertEqual(track.artist, "Test Artist")
- XCTAssertEqual(track.fileName, "test.mp3")
- XCTAssertEqual(track.formattedDuration, "3:00")
- XCTAssertFalse(track.isAnalyzed)
- XCTAssertNil(track.bpm)
- }
- func testTrackDefaults() {
- let track = Track(title: "Minimal", filePath: "Music/minimal.wav")
- XCTAssertEqual(track.artist, "")
- XCTAssertEqual(track.album, "")
- XCTAssertEqual(track.genre, "")
- XCTAssertEqual(track.playCount, 0)
- XCTAssertEqual(track.rating, 0)
- XCTAssertNil(track.lastPlayed)
- XCTAssertNil(track.color)
- XCTAssertNil(track.waveformData)
- XCTAssertEqual(track.notes, "")
- XCTAssertTrue(track.cuePoints.isEmpty)
- }
- func testTrackFormattedBPM() {
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- XCTAssertEqual(track.formattedBPM, "—")
- track.bpm = 128.5
- XCTAssertEqual(track.formattedBPM, "128.5")
- }
- func testTrackFormattedDurationEdgeCases() {
- let track0 = Track(title: "Zero", filePath: "Music/z.mp3", duration: 0)
- XCTAssertEqual(track0.formattedDuration, "0:00")
- let trackLong = Track(title: "Long", filePath: "Music/l.mp3", duration: 3661)
- XCTAssertEqual(trackLong.formattedDuration, "61:01")
- }
- func testFileNameExtraction() {
- let track = Track(title: "Song", filePath: "Music/subfolder/Artist - Song.flac")
- XCTAssertEqual(track.fileName, "Artist - Song.flac")
- }
- func testFileNameExplicit() {
- let track = Track(title: "Song", filePath: "Music/song.mp3", fileName: "custom.mp3")
- XCTAssertEqual(track.fileName, "custom.mp3")
- }
- func testTrackFileURL() {
- let track = Track(title: "Song", filePath: "Music/test.mp3")
- let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
- XCTAssertEqual(track.fileURL, docs.appendingPathComponent("Music/test.mp3"))
- }
- }
- // MARK: - Playlist Tests
- final class PlaylistTests: XCTestCase {
- func testPlaylistInit() {
- let playlist = Playlist(name: "Friday Mix")
- XCTAssertEqual(playlist.name, "Friday Mix")
- XCTAssertEqual(playlist.color, "#2196F3")
- XCTAssertEqual(playlist.trackCount, 0)
- XCTAssertNil(playlist.targetBPM)
- XCTAssertTrue(playlist.entries.isEmpty)
- XCTAssertNil(playlist.folder)
- }
- func testPlaylistAddTrack() {
- let playlist = Playlist(name: "My Mix")
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- XCTAssertEqual(playlist.trackCount, 0)
- playlist.addTrack(track, crossfadeDuration: 2.0)
- XCTAssertEqual(playlist.trackCount, 1)
- let entry = playlist.sortedEntries.first
- XCTAssertEqual(entry?.position, 0)
- XCTAssertEqual(entry?.crossfadeDuration, 2.0)
- }
- func testPlaylistAddMultipleTracks() {
- let playlist = Playlist(name: "Mix")
- let t1 = Track(title: "First", filePath: "Music/first.mp3", duration: 200)
- let t2 = Track(title: "Second", filePath: "Music/second.mp3", duration: 180)
- let t3 = Track(title: "Third", filePath: "Music/third.mp3", duration: 240)
- playlist.addTrack(t1)
- playlist.addTrack(t2)
- playlist.addTrack(t3)
- XCTAssertEqual(playlist.trackCount, 3)
- let sorted = playlist.sortedEntries
- XCTAssertEqual(sorted[0].track?.title, "First")
- XCTAssertEqual(sorted[1].track?.title, "Second")
- XCTAssertEqual(sorted[2].track?.title, "Third")
- XCTAssertEqual(sorted[0].position, 0)
- XCTAssertEqual(sorted[1].position, 1)
- XCTAssertEqual(sorted[2].position, 2)
- }
- func testPlaylistTotalDuration() {
- 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)
- playlist.addTrack(t1)
- playlist.addTrack(t2)
- XCTAssertEqual(playlist.totalDuration, 380, accuracy: 0.01)
- }
- func testPlaylistFormattedTotalDuration() {
- let playlist = Playlist(name: "Mix")
- playlist.addTrack(Track(title: "A", filePath: "Music/a.mp3", duration: 3700))
- XCTAssertTrue(playlist.formattedTotalDuration.contains(":"))
- }
- func testPlaylistRemoveEntry() {
- let playlist = Playlist(name: "Mix")
- 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")
- playlist.addTrack(t1)
- playlist.addTrack(t2)
- playlist.addTrack(t3)
- playlist.removeEntry(at: 1) // Remove "B"
- XCTAssertEqual(playlist.trackCount, 2)
- let sorted = playlist.sortedEntries
- XCTAssertEqual(sorted[0].track?.title, "A")
- XCTAssertEqual(sorted[1].track?.title, "C")
- // Positions should be re-indexed
- XCTAssertEqual(sorted[0].position, 0)
- XCTAssertEqual(sorted[1].position, 1)
- }
- func testPlaylistMoveEntry() {
- let playlist = Playlist(name: "Mix")
- 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")
- playlist.addTrack(t1)
- playlist.addTrack(t2)
- playlist.addTrack(t3)
- // Move C from position 2 to position 0
- playlist.moveEntry(from: 2, to: 0)
- let sorted = playlist.sortedEntries
- XCTAssertEqual(sorted[0].track?.title, "C")
- XCTAssertEqual(sorted[1].track?.title, "A")
- XCTAssertEqual(sorted[2].track?.title, "B")
- }
- func testPlaylistDateModifiedUpdates() {
- let playlist = Playlist(name: "Mix")
- let originalDate = playlist.dateModified
- // Small delay to ensure date changes
- let track = Track(title: "Song", filePath: "Music/song.mp3")
- playlist.addTrack(track)
- XCTAssertGreaterThanOrEqual(playlist.dateModified, originalDate)
- }
- }
- // MARK: - PlaylistEntry Tests
- final class PlaylistEntryTests: XCTestCase {
- func testEntryEffectiveDuration() {
- let track = Track(title: "Song", filePath: "Music/song.mp3", duration: 300)
- let entry = PlaylistEntry(position: 0, track: track)
- XCTAssertEqual(entry.effectiveDuration, 300, accuracy: 0.01)
- }
- func testEntryEffectiveDurationWithOffsets() {
- let track = Track(title: "Song", filePath: "Music/song.mp3", duration: 300)
- let entry = PlaylistEntry(position: 0, track: track, startOffset: 10, endOffset: 280)
- XCTAssertEqual(entry.effectiveDuration, 270, accuracy: 0.01)
- }
- func testEntryEffectiveDurationNoTrack() {
- let entry = PlaylistEntry(position: 0, track: nil)
- XCTAssertEqual(entry.effectiveDuration, 0)
- }
- func testEntryDefaults() {
- let entry = PlaylistEntry(position: 5, track: nil)
- XCTAssertEqual(entry.position, 5)
- XCTAssertEqual(entry.crossfadeDuration, 0)
- XCTAssertEqual(entry.startOffset, 0)
- XCTAssertEqual(entry.endOffset, 0)
- XCTAssertEqual(entry.gainAdjustment, 0)
- XCTAssertEqual(entry.notes, "")
- }
- }
- // MARK: - CuePoint Tests
- final class CuePointTests: XCTestCase {
- func testCuePointFormatTime() {
- XCTAssertEqual(CuePoint.formatTime(0), "00:00.000")
- XCTAssertEqual(CuePoint.formatTime(125.456), "02:05.456")
- XCTAssertEqual(CuePoint.formatTime(59.999), "00:59.999")
- }
- func testCuePointInit() {
- let cue = CuePoint(name: "Drop", timestamp: 45.5, type: .drop)
- XCTAssertEqual(cue.name, "Drop")
- XCTAssertEqual(cue.timestamp, 45.5)
- XCTAssertEqual(cue.type, .drop)
- XCTAssertFalse(cue.isRegion)
- }
- func testCuePointRegion() {
- let cue = CuePoint(name: "Verse", timestamp: 30, endTimestamp: 90, type: .verse)
- XCTAssertTrue(cue.isRegion)
- XCTAssertEqual(cue.endTimestamp, 90)
- }
- func testCuePointComparable() {
- let a = CuePoint(timestamp: 10)
- let b = CuePoint(timestamp: 20)
- let c = CuePoint(timestamp: 5)
- XCTAssertTrue(c < a)
- XCTAssertTrue(a < b)
- let sorted = [b, a, c].sorted()
- XCTAssertEqual(sorted.map(\.timestamp), [5, 10, 20])
- }
- func testCuePointTypes() {
- XCTAssertEqual(CuePointType.allCases.count, 11)
- XCTAssertEqual(CuePointType.marker.rawValue, "Marker")
- XCTAssertEqual(CuePointType.fadeOut.rawValue, "Fade Out")
- }
- }
- // MARK: - PlaylistFolder Tests
- final class PlaylistFolderTests: XCTestCase {
- func testFolderInit() {
- let folder = PlaylistFolder(name: "My Mixes")
- XCTAssertEqual(folder.name, "My Mixes")
- XCTAssertTrue(folder.isExpanded)
- XCTAssertTrue(folder.playlists.isEmpty)
- XCTAssertEqual(folder.totalTrackCount, 0)
- }
- }
- // MARK: - AppState Tests
- final class AppStateTests: XCTestCase {
- override func tearDown() {
- // Clean up UserDefaults
- UserDefaults.standard.removeObject(forKey: "appState.lastPlaylistID")
- UserDefaults.standard.removeObject(forKey: "appState.lastEntryID")
- UserDefaults.standard.removeObject(forKey: "appState.lastTrackFilePath")
- UserDefaults.standard.removeObject(forKey: "appState.lastPlaybackTime")
- }
- func testSaveAndLoadPlaylistID() {
- let id = UUID()
- AppState.saveLastPlaylist(id: id)
- XCTAssertEqual(AppState.lastPlaylistID, id)
- }
- func testSaveAndLoadEntryID() {
- let id = UUID()
- AppState.saveLastEntry(id: id)
- XCTAssertEqual(AppState.lastEntryID, id)
- }
- func testSaveAndLoadTrackFilePath() {
- AppState.saveLastTrack(filePath: "Music/test.mp3")
- XCTAssertEqual(AppState.lastTrackFilePath, "Music/test.mp3")
- }
- func testSaveAndLoadPlaybackTime() {
- AppState.savePlaybackTime(42.5)
- XCTAssertEqual(AppState.lastPlaybackTime, 42.5, accuracy: 0.01)
- }
- func testSavePlaybackStateAll() {
- let playlistID = UUID()
- let entryID = UUID()
- AppState.savePlaybackState(
- playlistID: playlistID,
- entryID: entryID,
- trackFilePath: "Music/combined.flac",
- playbackTime: 99.9
- )
- XCTAssertEqual(AppState.lastPlaylistID, playlistID)
- XCTAssertEqual(AppState.lastEntryID, entryID)
- XCTAssertEqual(AppState.lastTrackFilePath, "Music/combined.flac")
- XCTAssertEqual(AppState.lastPlaybackTime, 99.9, accuracy: 0.01)
- }
- func testDefaultsAreNil() {
- XCTAssertNil(AppState.lastPlaylistID)
- XCTAssertNil(AppState.lastEntryID)
- }
- }
- // MARK: - Sync Encoding/Decoding Tests
- final class SyncTests: XCTestCase {
- func testSyncPayloadRoundTrip() throws {
- let playlist = Playlist(name: "Test Mix", notes: "Great tracks", color: "#FF5722")
- playlist.targetBPM = 128.0
- let t1 = Track(title: "Song A", artist: "Artist A", filePath: "Music/a.mp3", fileName: "a.mp3", duration: 200)
- t1.bpm = 126
- t1.musicalKey = "Am"
- let t2 = Track(title: "Song B", artist: "Artist B", filePath: "Music/b.flac", fileName: "b.flac", duration: 180)
- playlist.addTrack(t1, crossfadeDuration: 3.0)
- playlist.addTrack(t2, crossfadeDuration: 2.0)
- let syncPlaylist = SyncPlaylist(from: playlist)
- let payload = SyncPayload(version: 1, exportedAt: Date(), exportedFrom: "Test", playlists: [syncPlaylist])
- let encoder = JSONEncoder()
- encoder.dateEncodingStrategy = .iso8601
- encoder.outputFormatting = .prettyPrinted
- let data = try encoder.encode(payload)
- let decoder = JSONDecoder()
- decoder.dateDecodingStrategy = .iso8601
- let decoded = try decoder.decode(SyncPayload.self, from: data)
- XCTAssertEqual(decoded.version, 1)
- XCTAssertEqual(decoded.playlists.count, 1)
- let dp = decoded.playlists[0]
- XCTAssertEqual(dp.name, "Test Mix")
- XCTAssertEqual(dp.notes, "Great tracks")
- XCTAssertEqual(dp.color, "#FF5722")
- XCTAssertEqual(dp.targetBPM, 128.0)
- XCTAssertEqual(dp.entries.count, 2)
- XCTAssertEqual(dp.entries[0].filename, "a.mp3")
- XCTAssertEqual(dp.entries[0].title, "Song A")
- XCTAssertEqual(dp.entries[0].artist, "Artist A")
- XCTAssertEqual(dp.entries[0].bpm, 126)
- XCTAssertEqual(dp.entries[0].musicalKey, "Am")
- XCTAssertEqual(dp.entries[0].crossfadeDuration, 3.0)
- XCTAssertEqual(dp.entries[1].filename, "b.flac")
- XCTAssertEqual(dp.entries[1].title, "Song B")
- }
- func testSyncEmptyPlaylist() throws {
- let playlist = Playlist(name: "Empty")
- let sp = SyncPlaylist(from: playlist)
- XCTAssertTrue(sp.entries.isEmpty)
- let payload = SyncPayload(version: 1, exportedAt: Date(), exportedFrom: "iPhone", playlists: [sp])
- let encoder = JSONEncoder()
- encoder.dateEncodingStrategy = .iso8601
- let data = try encoder.encode(payload)
- let decoded = try JSONDecoder().apply { $0.dateDecodingStrategy = .iso8601 }.decode(SyncPayload.self, from: data)
- XCTAssertEqual(decoded.playlists[0].entries.count, 0)
- }
- func testSyncEntryWithNoTrack() {
- let entry = PlaylistEntry(position: 0, track: nil, notes: "Missing track")
- let syncEntry = SyncEntry(from: entry)
- XCTAssertEqual(syncEntry.filename, "unknown")
- XCTAssertEqual(syncEntry.title, "Unknown")
- XCTAssertEqual(syncEntry.notes, "Missing track")
- }
- func testSyncMultiplePlaylists() throws {
- let p1 = Playlist(name: "Mix 1")
- let p2 = Playlist(name: "Mix 2")
- p1.addTrack(Track(title: "Song", filePath: "Music/s.mp3"))
- let payload = SyncPayload(
- version: 1,
- exportedAt: Date(),
- exportedFrom: "iPad",
- playlists: [SyncPlaylist(from: p1), SyncPlaylist(from: p2)]
- )
- let encoder = JSONEncoder()
- encoder.dateEncodingStrategy = .iso8601
- let data = try encoder.encode(payload)
- let decoder = JSONDecoder()
- decoder.dateDecodingStrategy = .iso8601
- let decoded = try decoder.decode(SyncPayload.self, from: data)
- XCTAssertEqual(decoded.playlists.count, 2)
- XCTAssertEqual(decoded.exportedFrom, "iPad")
- }
- }
- // MARK: - MetadataService Tests
- final class MetadataServiceTests: XCTestCase {
- func testSupportedFormats() {
- let supported = ["mp3", "wav", "aif", "aiff", "flac", "m4a", "aac", "caf", "alac", "ogg"]
- for ext in supported {
- XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.\(ext)")),
- "\(ext) should be supported")
- }
- }
- func testUnsupportedFormats() {
- let unsupported = ["txt", "jpg", "pdf", "mp4", "wma", "doc", "zip"]
- for ext in unsupported {
- XCTAssertFalse(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.\(ext)")),
- "\(ext) should not be supported")
- }
- }
- func testCaseInsensitive() {
- XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.MP3")))
- XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.Flac")))
- XCTAssertTrue(MetadataService.isSupportedAudioFile(URL(fileURLWithPath: "/test.WAV")))
- }
- }
- // MARK: - AppTheme Tests
- final class AppThemeTests: XCTestCase {
- func testDefaultSkin() {
- let theme = AppTheme()
- XCTAssertNotNil(theme.currentSkin)
- }
- func testWinampSkin() {
- let theme = AppTheme()
- theme.currentSkin = .winamp
- XCTAssertTrue(theme.useDarkMode)
- XCTAssertEqual(theme.preferredColorScheme, .dark)
- XCTAssertEqual(theme.cornerRadius, 4)
- }
- func testFoobarSkin() {
- let theme = AppTheme()
- theme.currentSkin = .foobarLight
- XCTAssertFalse(theme.useDarkMode)
- XCTAssertEqual(theme.preferredColorScheme, .light)
- XCTAssertEqual(theme.cornerRadius, 6)
- }
- func testFoobarDarkSkin() {
- let theme = AppTheme()
- theme.currentSkin = .foobarDark
- XCTAssertTrue(theme.useDarkMode)
- XCTAssertEqual(theme.preferredColorScheme, .dark)
- }
- func testVinylSkin() {
- let theme = AppTheme()
- theme.currentSkin = .vinyl
- XCTAssertTrue(theme.useDarkMode)
- XCTAssertEqual(theme.preferredColorScheme, .dark)
- }
- func testObsidianSkin() {
- let theme = AppTheme()
- theme.currentSkin = .obsidian
- XCTAssertTrue(theme.useDarkMode)
- XCTAssertEqual(theme.cornerRadius, 12)
- }
- func testWmpSkin() {
- let theme = AppTheme()
- theme.currentSkin = .wmp
- XCTAssertTrue(theme.useDarkMode)
- XCTAssertEqual(theme.cornerRadius, 8)
- }
- func testSkinCount() {
- XCTAssertEqual(AppTheme.Skin.allCases.count, 7)
- }
- func testSkinPersistence() {
- let theme = AppTheme()
- theme.currentSkin = .foobarLight
- XCTAssertEqual(UserDefaults.standard.string(forKey: "appThemeSkin"), "foobar Light")
- theme.currentSkin = .winamp
- XCTAssertEqual(UserDefaults.standard.string(forKey: "appThemeSkin"), "Winamp")
- }
- }
- // MARK: - Color Extension Tests
- final class ColorTests: XCTestCase {
- func testValidHexColors() {
- XCTAssertNotNil(Color(hex: "#FF0000"))
- XCTAssertNotNil(Color(hex: "00FF00"))
- XCTAssertNotNil(Color(hex: "#2196F3"))
- }
- func testInvalidHexColors() {
- XCTAssertNil(Color(hex: ""))
- XCTAssertNil(Color(hex: "XYZ"))
- XCTAssertNil(Color(hex: "#12345")) // 5 chars
- }
- }
- // MARK: - Helper
- extension JSONDecoder {
- func apply(_ configure: (JSONDecoder) -> Void) -> JSONDecoder {
- configure(self)
- return self
- }
- }
|