| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- import XCTest
- import SwiftData
- @testable import MixBoard
- /// Tests for the Track model.
- final class TrackModelTests: XCTestCase {
- func testTrackCreation() {
- let track = Track(
- title: "Test Song",
- artist: "Test Artist",
- album: "Test Album",
- genre: "Rock",
- filePath: "/tmp/test.mp3",
- duration: 180,
- sampleRate: 44100,
- bitDepth: 16,
- channels: 2,
- fileFormat: "MP3",
- fileSizeBytes: 5_000_000
- )
- XCTAssertEqual(track.title, "Test Song")
- XCTAssertEqual(track.artist, "Test Artist")
- XCTAssertEqual(track.album, "Test Album")
- XCTAssertEqual(track.genre, "Rock")
- XCTAssertEqual(track.duration, 180)
- XCTAssertEqual(track.sampleRate, 44100)
- XCTAssertEqual(track.bitDepth, 16)
- XCTAssertEqual(track.channels, 2)
- XCTAssertEqual(track.fileFormat, "MP3")
- XCTAssertEqual(track.fileSizeBytes, 5_000_000)
- XCTAssertEqual(track.rating, 0)
- XCTAssertEqual(track.playCount, 0)
- XCTAssertFalse(track.isAnalyzed)
- XCTAssertNil(track.bpm)
- XCTAssertNil(track.musicalKey)
- XCTAssertTrue(track.cuePoints.isEmpty)
- }
- func testFormattedDuration() {
- let track = Track(title: "T", filePath: "/t", duration: 185)
- XCTAssertEqual(track.formattedDuration, "3:05")
- let track2 = Track(title: "T", filePath: "/t", duration: 60)
- XCTAssertEqual(track2.formattedDuration, "1:00")
- let track3 = Track(title: "T", filePath: "/t", duration: 0)
- XCTAssertEqual(track3.formattedDuration, "0:00")
- }
- func testFormattedBPM() {
- let track = Track(title: "T", filePath: "/t")
- XCTAssertEqual(track.formattedBPM, "—")
- track.bpm = 128.5
- XCTAssertEqual(track.formattedBPM, "128.5")
- }
- func testFileURL() {
- let track = Track(title: "T", filePath: "/Users/test/music/song.mp3")
- XCTAssertEqual(track.fileURL.path, "/Users/test/music/song.mp3")
- }
- func testFormattedFileSize() {
- let track = Track(title: "T", filePath: "/t", fileSizeBytes: 1_048_576)
- let formatted = track.formattedFileSize
- // Should be approximately "1 MB"
- XCTAssertTrue(formatted.contains("MB") || formatted.contains("1"))
- }
- }
- /// Tests for the CuePoint model.
- final class CuePointModelTests: XCTestCase {
- func testCuePointCreation() {
- let cue = CuePoint(name: "Drop", timestamp: 30.5, type: .drop)
- XCTAssertEqual(cue.name, "Drop")
- XCTAssertEqual(cue.timestamp, 30.5)
- XCTAssertEqual(cue.type, .drop)
- XCTAssertNil(cue.endTimestamp)
- XCTAssertFalse(cue.isRegion)
- }
- func testCuePointRegion() {
- let cue = CuePoint(name: "Verse", timestamp: 10, endTimestamp: 40, type: .verse)
- XCTAssertTrue(cue.isRegion)
- XCTAssertEqual(cue.endTimestamp, 40)
- }
- func testCuePointComparable() {
- let c1 = CuePoint(timestamp: 10)
- let c2 = CuePoint(timestamp: 20)
- let c3 = CuePoint(timestamp: 5)
- let sorted = [c1, c2, c3].sorted()
- XCTAssertEqual(sorted[0].timestamp, 5)
- XCTAssertEqual(sorted[1].timestamp, 10)
- XCTAssertEqual(sorted[2].timestamp, 20)
- }
- func testFormattedTimestamp() {
- let cue = CuePoint(timestamp: 65.123)
- XCTAssertEqual(cue.formattedTimestamp, "01:05.123")
- }
- func testAllCuePointTypes() {
- XCTAssertEqual(CuePointType.allCases.count, 11)
- XCTAssertEqual(CuePointType.marker.rawValue, "Marker")
- XCTAssertEqual(CuePointType.drop.rawValue, "Drop")
- XCTAssertEqual(CuePointType.fadeOut.rawValue, "Fade Out")
- }
- }
- /// Tests for the Playlist model.
- final class PlaylistModelTests: XCTestCase {
- func testPlaylistCreation() {
- let pl = Playlist(name: "My Mix")
- XCTAssertEqual(pl.name, "My Mix")
- XCTAssertTrue(pl.entries.isEmpty)
- XCTAssertEqual(pl.trackCount, 0)
- XCTAssertEqual(pl.totalDuration, 0)
- XCTAssertNil(pl.targetBPM)
- }
- func testAddTrack() {
- let pl = Playlist(name: "Mix")
- let track = Track(title: "Song", filePath: "/t", duration: 120)
- pl.addTrack(track, crossfadeDuration: 2.0)
- XCTAssertEqual(pl.trackCount, 1)
- XCTAssertEqual(pl.entries.count, 1)
- XCTAssertEqual(pl.entries.first?.track?.title, "Song")
- XCTAssertEqual(pl.entries.first?.crossfadeDuration, 2.0)
- XCTAssertEqual(pl.entries.first?.position, 0)
- }
- func testSortedEntries() {
- let pl = Playlist(name: "Mix")
- let t1 = Track(title: "First", filePath: "/1", duration: 60)
- let t2 = Track(title: "Second", filePath: "/2", duration: 90)
- let t3 = Track(title: "Third", filePath: "/3", duration: 45)
- pl.addTrack(t1)
- pl.addTrack(t2)
- pl.addTrack(t3)
- let sorted = pl.sortedEntries
- XCTAssertEqual(sorted.count, 3)
- XCTAssertEqual(sorted[0].track?.title, "First")
- XCTAssertEqual(sorted[1].track?.title, "Second")
- XCTAssertEqual(sorted[2].track?.title, "Third")
- }
- func testFormattedTotalDuration() {
- let pl = Playlist(name: "Mix")
- pl.addTrack(Track(title: "A", filePath: "/a", duration: 3661))
- let formatted = pl.formattedTotalDuration
- XCTAssertEqual(formatted, "1:01:01")
- }
- func testFormattedTotalDurationShort() {
- let pl = Playlist(name: "Mix")
- pl.addTrack(Track(title: "A", filePath: "/a", duration: 125))
- XCTAssertEqual(pl.formattedTotalDuration, "2:05")
- }
- }
- /// Tests for PlaylistFolder model.
- final class PlaylistFolderTests: XCTestCase {
- func testFolderCreation() {
- let folder = PlaylistFolder(name: "Hip-Hop")
- XCTAssertEqual(folder.name, "Hip-Hop")
- XCTAssertTrue(folder.playlists.isEmpty)
- XCTAssertTrue(folder.isExpanded)
- XCTAssertEqual(folder.totalTrackCount, 0)
- }
- func testFolderWithPlaylists() {
- let folder = PlaylistFolder(name: "Mixes")
- let pl1 = Playlist(name: "Mix 1")
- let pl2 = Playlist(name: "Mix 2")
- pl1.addTrack(Track(title: "A", filePath: "/a", duration: 60))
- pl2.addTrack(Track(title: "B", filePath: "/b", duration: 90))
- pl2.addTrack(Track(title: "C", filePath: "/c", duration: 45))
- pl1.folder = folder
- pl2.folder = folder
- folder.playlists = [pl1, pl2]
- XCTAssertEqual(folder.playlists.count, 2)
- XCTAssertEqual(folder.totalTrackCount, 3)
- }
- func testPlaylistFolderAssignment() {
- let folder = PlaylistFolder(name: "Test")
- let pl = Playlist(name: "My Playlist")
- XCTAssertNil(pl.folder)
- pl.folder = folder
- XCTAssertNotNil(pl.folder)
- XCTAssertEqual(pl.folder?.name, "Test")
- // Remove from folder
- pl.folder = nil
- XCTAssertNil(pl.folder)
- }
- func testFolderExpanded() {
- let folder = PlaylistFolder(name: "F")
- XCTAssertTrue(folder.isExpanded)
- folder.isExpanded = false
- XCTAssertFalse(folder.isExpanded)
- }
- }
- /// Tests for PlaylistEntry model.
- final class PlaylistEntryModelTests: XCTestCase {
- func testEffectiveDuration() {
- let track = Track(title: "T", filePath: "/t", duration: 180)
- let entry = PlaylistEntry(position: 0, track: track, startOffset: 10, endOffset: 170)
- XCTAssertEqual(entry.effectiveDuration, 160)
- }
- func testEffectiveDurationNoOffsets() {
- let track = Track(title: "T", filePath: "/t", duration: 180)
- let entry = PlaylistEntry(position: 0, track: track)
- XCTAssertEqual(entry.effectiveDuration, 180)
- }
- func testGainDefaults() {
- let entry = PlaylistEntry(position: 0, track: nil)
- XCTAssertEqual(entry.gainAdjustment, 0)
- XCTAssertEqual(entry.crossfadeDuration, 0)
- }
- }
- /// Tests for PlaylistViewConfig.
- final class PlaylistViewConfigTests: XCTestCase {
- func testDefaultColumns() {
- let defaultCols = PlaylistViewConfig.defaultColumns
- XCTAssertTrue(defaultCols.contains(.title))
- XCTAssertTrue(defaultCols.contains(.artist))
- XCTAssertTrue(defaultCols.contains(.duration))
- }
- func testToggleColumn() {
- let config = PlaylistViewConfig()
- let initialCount = config.visibleColumns.count
- // Toggle off a column that exists
- if config.isColumnVisible(.title) {
- config.toggleColumn(.title)
- XCTAssertFalse(config.isColumnVisible(.title))
- XCTAssertEqual(config.visibleColumns.count, initialCount - 1)
- }
- // Toggle it back on
- config.toggleColumn(.title)
- XCTAssertTrue(config.isColumnVisible(.title))
- }
- func testResetToDefaults() {
- let config = PlaylistViewConfig()
- config.visibleColumns = [.title]
- config.showArtwork = false
- config.resetToDefaults()
- XCTAssertEqual(config.visibleColumns, PlaylistViewConfig.defaultColumns)
- XCTAssertTrue(config.showArtwork)
- }
- func testArtworkSizes() {
- XCTAssertEqual(PlaylistViewConfig.ArtworkSize.small.points, 32)
- XCTAssertEqual(PlaylistViewConfig.ArtworkSize.medium.points, 48)
- XCTAssertEqual(PlaylistViewConfig.ArtworkSize.large.points, 64)
- }
- }
- /// Tests for GroupTemplateResolver.
- final class GroupTemplateResolverTests: XCTestCase {
- func testEmptyTemplateReturnsEmpty() {
- let track = Track(title: "Test", artist: "Artist", album: "Album", filePath: "/music/test.mp3")
- let result = GroupTemplateResolver.resolve(template: "", for: track)
- XCTAssertEqual(result, "")
- }
- func testAlbumDateTemplate() {
- let track = Track(title: "Test", artist: "Artist", album: "My Album", filePath: "/music/test.mp3")
- track.year = 1995
- let result = GroupTemplateResolver.resolve(template: "{Album} ({Date})", for: track)
- XCTAssertEqual(result, "My Album (1995)")
- }
- func testAlbumDateTemplateNoYear() {
- let track = Track(title: "Test", artist: "Artist", album: "My Album", filePath: "/music/test.mp3")
- let result = GroupTemplateResolver.resolve(template: "{Album} ({Date})", for: track)
- // No year → empty brackets cleaned up
- XCTAssertEqual(result, "My Album")
- }
- func testArtistAlbumTemplate() {
- let track = Track(title: "Song", artist: "Fagner", album: "Manera Fru Fru", filePath: "/music/test.mp3")
- let result = GroupTemplateResolver.resolve(template: "{Artist} — {Album}", for: track)
- XCTAssertEqual(result, "Fagner — Manera Fru Fru")
- }
- func testUnknownFallbacks() {
- let track = Track(title: "Test", filePath: "/music/test.mp3")
- let result = GroupTemplateResolver.resolve(template: "{Artist}", for: track)
- XCTAssertEqual(result, "Unknown Artist")
- }
- func testFolderPlaceholder() {
- let track = Track(title: "Test", filePath: "/music/batch1/test.mp3")
- let result = GroupTemplateResolver.resolve(template: "{Folder}", for: track)
- XCTAssertEqual(result, "batch1")
- }
- func testPresetsExist() {
- XCTAssertTrue(GroupTemplateResolver.presets.count >= 9)
- XCTAssertEqual(GroupTemplateResolver.presets.first?.template, "")
- }
- }
- /// Tests for AppTheme.
- final class AppThemeTests: XCTestCase {
- private var savedSkinRaw: String?
- override func setUp() {
- super.setUp()
- // Save current skin so tests don't pollute user preferences
- savedSkinRaw = UserDefaults.standard.string(forKey: "appThemeSkin")
- }
- override func tearDown() {
- // Restore original skin
- if let raw = savedSkinRaw {
- UserDefaults.standard.set(raw, forKey: "appThemeSkin")
- } else {
- UserDefaults.standard.removeObject(forKey: "appThemeSkin")
- }
- super.tearDown()
- }
- func testDefaultSkin() {
- let theme = AppTheme()
- XCTAssertTrue(AppTheme.Skin.allCases.contains(theme.currentSkin))
- }
- func testSkinSwitch() {
- let theme = AppTheme()
- theme.currentSkin = .ocean
- XCTAssertEqual(theme.currentSkin, .ocean)
- theme.currentSkin = .warm
- XCTAssertEqual(theme.currentSkin, .warm)
- theme.currentSkin = .winampClassic
- XCTAssertEqual(theme.currentSkin, .winampClassic)
- theme.currentSkin = .foobarDark
- XCTAssertEqual(theme.currentSkin, .foobarDark)
- theme.currentSkin = .foobarLight
- XCTAssertEqual(theme.currentSkin, .foobarLight)
- theme.currentSkin = .win95
- XCTAssertEqual(theme.currentSkin, .win95)
- }
- func testAllSkinsAvailable() {
- // 6 modern + 8 retro = 14
- XCTAssertEqual(AppTheme.Skin.allCases.count, 14)
- }
- func testAllSkinsApplyWithoutCrash() {
- let theme = AppTheme()
- for skin in AppTheme.Skin.allCases {
- theme.currentSkin = skin
- // Verify key properties are set to reasonable values
- XCTAssertGreaterThan(theme.seekbarHeight, 0, "Seekbar height should be > 0 for \(skin.rawValue)")
- XCTAssertGreaterThan(theme.dataFontSize, 0, "Font size should be > 0 for \(skin.rawValue)")
- XCTAssertGreaterThan(theme.rowHeight, 0, "Row height should be > 0 for \(skin.rawValue)")
- }
- }
- func testRetroSkinsHaveDistinctColors() {
- let theme = AppTheme()
- // Winamp Classic should have green text
- theme.currentSkin = .winampClassic
- // Just verify it doesn't crash and has accent set
- XCTAssertEqual(theme.currentSkin, .winampClassic)
- // foobar2000 should be light/system-like
- theme.currentSkin = .foobarLight
- XCTAssertEqual(theme.rowHeight, 18) // foobar has tighter rows
- // Win95 should have thicker seekbar
- theme.currentSkin = .win95
- XCTAssertEqual(theme.seekbarHeight, 10)
- // XP Luna
- theme.currentSkin = .xpLuna
- XCTAssertEqual(theme.seekbarHeight, 10)
- // Mac OS 9
- theme.currentSkin = .macOSClassic
- XCTAssertEqual(theme.dataFontSize, 12) // slightly larger like classic Mac
- }
- func testSeekbarHeight() {
- let theme = AppTheme()
- theme.currentSkin = .dark
- XCTAssertEqual(theme.seekbarHeight, 8)
- }
- }
- /// Tests for AppState persistence.
- final class AppStateTests: XCTestCase {
- private func clearState() {
- UserDefaults.standard.removeObject(forKey: "appState.lastPlaylistID")
- UserDefaults.standard.removeObject(forKey: "appState.lastEntryID")
- UserDefaults.standard.removeObject(forKey: "appState.lastTrackFilePath")
- UserDefaults.standard.removeObject(forKey: "appState.lastPlaybackTime")
- }
- override func setUp() {
- super.setUp()
- clearState()
- }
- override func tearDown() {
- clearState()
- super.tearDown()
- }
- 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 testSaveAndLoadTrackPath() {
- AppState.saveLastTrack(filePath: "/music/test.mp3")
- XCTAssertEqual(AppState.lastTrackFilePath, "/music/test.mp3")
- }
- func testSaveAndLoadPlaybackTime() {
- AppState.savePlaybackTime(123.456)
- XCTAssertEqual(AppState.lastPlaybackTime, 123.456, accuracy: 0.001)
- }
- func testSavePlaybackStateAll() {
- let plID = UUID()
- let entryID = UUID()
- AppState.savePlaybackState(
- playlistID: plID,
- entryID: entryID,
- trackFilePath: "/test.wav",
- playbackTime: 42.5
- )
- XCTAssertEqual(AppState.lastPlaylistID, plID)
- XCTAssertEqual(AppState.lastEntryID, entryID)
- XCTAssertEqual(AppState.lastTrackFilePath, "/test.wav")
- XCTAssertEqual(AppState.lastPlaybackTime, 42.5, accuracy: 0.001)
- }
- func testDefaultsAreNil() {
- XCTAssertNil(AppState.lastPlaylistID)
- XCTAssertNil(AppState.lastEntryID)
- }
- }
|