import XCTest @testable import MixBoard /// Tests for FileNameTemplate. final class FileNameTemplateTests: XCTestCase { private func makeTrack( title: String = "Spot Rusherz", artist: String = "Raekwon", album: String = "Only Built 4 Cuban Linx", genre: String = "Hip-Hop", bpm: Double? = 95, key: String? = "Dm", format: String = "MP3", duration: TimeInterval = 193 ) -> Track { let t = Track(title: title, artist: artist, album: album, genre: genre, filePath: "/fake.\(format.lowercased())", duration: duration, fileFormat: format) t.bpm = bpm t.musicalKey = key return t } // MARK: - Basic Templates func testDefaultTemplate() { let track = makeTrack() let result = FileNameTemplate.generate(template: FileNameTemplate.defaultTemplate, track: track, playlistIndex: 0, totalTracks: 10) XCTAssertTrue(result.contains("Raekwon")) XCTAssertTrue(result.contains("Only Built 4 Cuban Linx")) XCTAssertTrue(result.contains("Spot Rusherz")) } func testTrackNumberPadding() { let track = makeTrack() let r1 = FileNameTemplate.generate(template: "{track} {title}", track: track, playlistIndex: 0, totalTracks: 10) XCTAssertTrue(r1.hasPrefix("01")) let r2 = FileNameTemplate.generate(template: "{track} {title}", track: track, playlistIndex: 4, totalTracks: 150) XCTAssertTrue(r2.hasPrefix("005")) } func testAllVariables() { let track = makeTrack() let all = "{track} {artist} {album} {title} {genre} {bpm} {key} {duration} {format} {samplerate} {bitdepth}" let result = FileNameTemplate.generate(template: all, track: track, playlistIndex: 2, totalTracks: 10) XCTAssertTrue(result.contains("03")) XCTAssertTrue(result.contains("Raekwon")) XCTAssertTrue(result.contains("Only Built 4 Cuban Linx")) XCTAssertTrue(result.contains("Spot Rusherz")) XCTAssertTrue(result.contains("Hip-Hop")) XCTAssertTrue(result.contains("95")) XCTAssertTrue(result.contains("Dm")) XCTAssertTrue(result.contains("MP3")) // Duration colon gets sanitized to _ for filenames, so check for digits XCTAssertTrue(result.contains("3") && result.contains("13"), "Should contain duration digits") } func testEmptyFields() { let track = makeTrack(artist: "", album: "", bpm: nil, key: nil) let result = FileNameTemplate.generate(template: "{artist} - {album} - {title}", track: track, playlistIndex: 0, totalTracks: 1) // Should clean up double separators XCTAssertTrue(result.contains("Spot Rusherz")) XCTAssertFalse(result.contains(" - - ")) } func testFilesystemSanitization() { let track = makeTrack(title: "What/Ever: The *Best?") let result = FileNameTemplate.generate(template: "{title}", track: track, playlistIndex: 0, totalTracks: 1) XCTAssertFalse(result.contains("/")) XCTAssertFalse(result.contains(":")) XCTAssertFalse(result.contains("*")) XCTAssertFalse(result.contains("?")) } func testPreview() { let preview = FileNameTemplate.preview(template: "{artist} - {title}") XCTAssertTrue(preview.contains("Raekwon")) XCTAssertTrue(preview.contains("Spot Rusherz")) } func testPresetsExist() { XCTAssertGreaterThanOrEqual(FileNameTemplate.presets.count, 4) for preset in FileNameTemplate.presets { XCTAssertFalse(preset.name.isEmpty) XCTAssertFalse(preset.template.isEmpty) // Each preset should generate a non-empty result let result = FileNameTemplate.preview(template: preset.template) XCTAssertGreaterThan(result.count, 0) } } func testAvailableVariables() { XCTAssertGreaterThanOrEqual(FileNameTemplate.availableVariables.count, 10) for v in FileNameTemplate.availableVariables { XCTAssertTrue(v.token.hasPrefix("{")) XCTAssertTrue(v.token.hasSuffix("}")) XCTAssertFalse(v.description.isEmpty) } } func testBPMOnlyWhenPresent() { let track = makeTrack(bpm: nil) let result = FileNameTemplate.generate(template: "{title} [{bpm}]", track: track, playlistIndex: 0, totalTracks: 1) // Empty BPM should result in clean output (brackets cleaned) XCTAssertFalse(result.contains("[]")) } } /// Tests for ExportOptions with fileNameTemplate. final class ExportOptionsTests: XCTestCase { func testDefaultOptionsHaveNoTemplate() { let opts = ExportOptions.default XCTAssertNil(opts.fileNameTemplate) } func testTemplateCanBeSet() { var opts = ExportOptions.default opts.fileNameTemplate = "{track} {artist} - {title}" XCTAssertEqual(opts.fileNameTemplate, "{track} {artist} - {title}") } } /// Tests for PlaylistViewModel status messages. final class StatusMessageTests: XCTestCase { @MainActor func testShowStatus() async { let vm = PlaylistViewModel() vm.showStatus("Test message", duration: 0.5) XCTAssertEqual(vm.statusMessage, "Test message") // Wait for auto-clear try? await Task.sleep(for: .seconds(0.7)) XCTAssertNil(vm.statusMessage) } @MainActor func testStatusOverwrite() { let vm = PlaylistViewModel() vm.showStatus("First") XCTAssertEqual(vm.statusMessage, "First") vm.showStatus("Second") XCTAssertEqual(vm.statusMessage, "Second") } }