Unit tests und nudging screen
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import Mobile_Music_Assistant
|
||||
|
||||
// MARK: - MAPlayer Decoding
|
||||
|
||||
@Suite("MAPlayer Decoding")
|
||||
struct MAPlayerDecodingTests {
|
||||
|
||||
private func decode(_ json: String) throws -> MAPlayer {
|
||||
try JSONDecoder().decode(MAPlayer.self, from: Data(json.utf8))
|
||||
}
|
||||
|
||||
@Test("Decodes all required fields correctly")
|
||||
func decodesFullPlayer() throws {
|
||||
let json = """
|
||||
{
|
||||
"player_id": "spotify://player/1",
|
||||
"name": "Living Room",
|
||||
"state": "playing",
|
||||
"volume_level": 75,
|
||||
"powered": true,
|
||||
"available": true
|
||||
}
|
||||
"""
|
||||
let player = try decode(json)
|
||||
#expect(player.playerId == "spotify://player/1")
|
||||
#expect(player.name == "Living Room")
|
||||
#expect(player.state == .playing)
|
||||
#expect(player.volume == 75)
|
||||
#expect(player.powered == true)
|
||||
#expect(player.available == true)
|
||||
}
|
||||
|
||||
@Test("Defaults powered and available when missing")
|
||||
func defaultsMissingBooleans() throws {
|
||||
let json = """
|
||||
{"player_id": "x", "name": "Test", "state": "idle"}
|
||||
"""
|
||||
let player = try decode(json)
|
||||
#expect(player.powered == false)
|
||||
#expect(player.available == false)
|
||||
}
|
||||
|
||||
@Test("Unknown state falls back to idle")
|
||||
func unknownStateFallsBackToIdle() throws {
|
||||
let json = """
|
||||
{"player_id": "x", "name": "Test", "state": "banana"}
|
||||
"""
|
||||
let player = try decode(json)
|
||||
#expect(player.state == .idle)
|
||||
}
|
||||
|
||||
@Test("Parses all known PlayerState values")
|
||||
func allPlayerStates() throws {
|
||||
for (raw, expected): (String, PlayerState) in [
|
||||
("idle", .idle), ("playing", .playing), ("paused", .paused)
|
||||
] {
|
||||
let json = """
|
||||
{"player_id": "x", "name": "T", "state": "\(raw)"}
|
||||
"""
|
||||
let player = try decode(json)
|
||||
#expect(player.state == expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Accepts integer or double for volume_level")
|
||||
func volumeIntOrDouble() throws {
|
||||
let jsonInt = #"{"player_id":"x","name":"T","state":"idle","volume_level":50}"#
|
||||
let jsonDbl = #"{"player_id":"x","name":"T","state":"idle","volume_level":50.7}"#
|
||||
let p1 = try decode(jsonInt)
|
||||
let p2 = try decode(jsonDbl)
|
||||
#expect(p1.volume == 50)
|
||||
#expect(p2.volume == 50)
|
||||
}
|
||||
|
||||
@Test("isGroupLeader is true when groupChilds is non-empty")
|
||||
func isGroupLeader() throws {
|
||||
let json = """
|
||||
{"player_id":"x","name":"T","state":"idle","group_childs":["a","b"]}
|
||||
"""
|
||||
let player = try decode(json)
|
||||
#expect(player.isGroupLeader == true)
|
||||
}
|
||||
|
||||
@Test("isGroupLeader is false when groupChilds is empty")
|
||||
func isNotGroupLeader() throws {
|
||||
let json = #"{"player_id":"x","name":"T","state":"idle","group_childs":[]}"#
|
||||
let player = try decode(json)
|
||||
#expect(player.isGroupLeader == false)
|
||||
}
|
||||
|
||||
@Test("isSyncMember is true when syncLeader is set")
|
||||
func isSyncMember() throws {
|
||||
let json = """
|
||||
{"player_id":"x","name":"T","state":"idle","sync_leader":"leader_id"}
|
||||
"""
|
||||
let player = try decode(json)
|
||||
#expect(player.isSyncMember == true)
|
||||
}
|
||||
|
||||
@Test("isSyncMember is false when syncLeader is nil")
|
||||
func isNotSyncMember() throws {
|
||||
let json = #"{"player_id":"x","name":"T","state":"idle"}"#
|
||||
let player = try decode(json)
|
||||
#expect(player.isSyncMember == false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MAMediaItem Decoding
|
||||
|
||||
@Suite("MAMediaItem Decoding")
|
||||
struct MAMediaItemDecodingTests {
|
||||
|
||||
private func decode(_ json: String) throws -> MAMediaItem {
|
||||
try JSONDecoder().decode(MAMediaItem.self, from: Data(json.utf8))
|
||||
}
|
||||
|
||||
@Test("Extracts imageUrl from metadata.images")
|
||||
func imageUrlFromMetadata() throws {
|
||||
let json = """
|
||||
{
|
||||
"uri": "spotify://track/1",
|
||||
"name": "Song",
|
||||
"metadata": {
|
||||
"images": [
|
||||
{"type": "thumb", "path": "/img/thumb.jpg"}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
let item = try decode(json)
|
||||
#expect(item.imageUrl == "/img/thumb.jpg")
|
||||
}
|
||||
|
||||
@Test("Extracts imageUrl from direct image field")
|
||||
func imageUrlFromDirectField() throws {
|
||||
let json = """
|
||||
{
|
||||
"uri": "spotify://track/1",
|
||||
"name": "Song",
|
||||
"image": {"type": "thumb", "path": "/direct.jpg"}
|
||||
}
|
||||
"""
|
||||
let item = try decode(json)
|
||||
#expect(item.imageUrl == "/direct.jpg")
|
||||
}
|
||||
|
||||
@Test("imageUrl is nil when no image data present")
|
||||
func imageUrlNilWhenMissing() throws {
|
||||
let json = #"{"uri":"x","name":"T"}"#
|
||||
let item = try decode(json)
|
||||
#expect(item.imageUrl == nil)
|
||||
}
|
||||
|
||||
@Test("Prefers metadata over direct image field")
|
||||
func prefersMetadataOverDirectImage() throws {
|
||||
let json = """
|
||||
{
|
||||
"uri": "x",
|
||||
"name": "T",
|
||||
"metadata": {"images": [{"type": "thumb", "path": "/meta.jpg"}]},
|
||||
"image": {"type": "thumb", "path": "/direct.jpg"}
|
||||
}
|
||||
"""
|
||||
let item = try decode(json)
|
||||
#expect(item.imageUrl == "/meta.jpg")
|
||||
}
|
||||
|
||||
@Test("Decodes artist name")
|
||||
func decodesArtistName() throws {
|
||||
let json = """
|
||||
{
|
||||
"uri": "x",
|
||||
"name": "Song",
|
||||
"artists": [{"item_id": "1", "uri": "a://artist/1", "name": "Queen"}]
|
||||
}
|
||||
"""
|
||||
let item = try decode(json)
|
||||
#expect(item.artists?.first?.name == "Queen")
|
||||
}
|
||||
|
||||
@Test("Accepts Int or Double for duration")
|
||||
func durationIntOrDouble() throws {
|
||||
let j1 = #"{"uri":"x","name":"T","duration":240}"#
|
||||
let j2 = #"{"uri":"x","name":"T","duration":240.9}"#
|
||||
let i1 = try decode(j1)
|
||||
let i2 = try decode(j2)
|
||||
#expect(i1.duration == 240)
|
||||
#expect(i2.duration == 240)
|
||||
}
|
||||
|
||||
@Test("favorite defaults to false when missing")
|
||||
func favoriteDefaultsFalse() throws {
|
||||
let json = #"{"uri":"x","name":"T"}"#
|
||||
let item = try decode(json)
|
||||
#expect(item.favorite == false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MAQueueItem Decoding
|
||||
|
||||
@Suite("MAQueueItem Decoding")
|
||||
struct MAQueueItemDecodingTests {
|
||||
|
||||
private func decode(_ json: String) throws -> MAQueueItem {
|
||||
try JSONDecoder().decode(MAQueueItem.self, from: Data(json.utf8))
|
||||
}
|
||||
|
||||
@Test("Decodes queue item with media item")
|
||||
func decodesWithMediaItem() throws {
|
||||
let json = """
|
||||
{
|
||||
"queue_item_id": "q1",
|
||||
"name": "Bohemian Rhapsody",
|
||||
"media_item": {"uri": "spotify://track/1", "name": "Bohemian Rhapsody"}
|
||||
}
|
||||
"""
|
||||
let item = try decode(json)
|
||||
#expect(item.queueItemId == "q1")
|
||||
#expect(item.name == "Bohemian Rhapsody")
|
||||
#expect(item.mediaItem?.name == "Bohemian Rhapsody")
|
||||
}
|
||||
|
||||
@Test("mediaItem is nil when absent from JSON")
|
||||
func mediaItemNilWhenAbsent() throws {
|
||||
let json = #"{"queue_item_id":"q1","name":"Track"}"#
|
||||
let item = try decode(json)
|
||||
#expect(item.mediaItem == nil)
|
||||
}
|
||||
|
||||
@Test("Tolerates malformed mediaItem without throwing")
|
||||
func toleratesMalformedMediaItem() throws {
|
||||
let json = #"{"queue_item_id":"q1","name":"T","media_item":"broken"}"#
|
||||
// Should decode successfully, mediaItem == nil
|
||||
let item = try decode(json)
|
||||
#expect(item.mediaItem == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PlayerState
|
||||
|
||||
@Suite("PlayerState")
|
||||
struct PlayerStateTests {
|
||||
|
||||
@Test("All raw values round-trip correctly")
|
||||
func rawValueRoundTrip() {
|
||||
let states: [(String, PlayerState)] = [
|
||||
("idle", .idle),
|
||||
("playing", .playing),
|
||||
("paused", .paused)
|
||||
]
|
||||
for (raw, state) in states {
|
||||
#expect(PlayerState(rawValue: raw) == state)
|
||||
#expect(state.rawValue == raw)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unknown raw value returns nil")
|
||||
func unknownRawValueIsNil() {
|
||||
#expect(PlayerState(rawValue: "streaming") == nil)
|
||||
#expect(PlayerState(rawValue: "") == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MAPlayerQueue Decoding
|
||||
|
||||
@Suite("MAPlayerQueue Decoding")
|
||||
struct MAPlayerQueueDecodingTests {
|
||||
|
||||
private func decode(_ json: String) throws -> MAPlayerQueue {
|
||||
try JSONDecoder().decode(MAPlayerQueue.self, from: Data(json.utf8))
|
||||
}
|
||||
|
||||
@Test("Decodes queue with currentItem")
|
||||
func decodesWithCurrentItem() throws {
|
||||
let json = """
|
||||
{
|
||||
"queue_id": "q://1",
|
||||
"current_index": 2,
|
||||
"elapsed_time": 45.5,
|
||||
"current_item": {"queue_item_id": "qi1", "name": "Track"}
|
||||
}
|
||||
"""
|
||||
let queue = try decode(json)
|
||||
#expect(queue.queueId == "q://1")
|
||||
#expect(queue.currentIndex == 2)
|
||||
#expect(queue.currentItem?.name == "Track")
|
||||
}
|
||||
|
||||
@Test("currentItem is nil when absent")
|
||||
func currentItemNilWhenAbsent() throws {
|
||||
let json = #"{"queue_id":"q://1"}"#
|
||||
let queue = try decode(json)
|
||||
#expect(queue.currentItem == nil)
|
||||
}
|
||||
|
||||
@Test("Tolerates malformed currentItem gracefully")
|
||||
func toleratesMalformedCurrentItem() throws {
|
||||
let json = #"{"queue_id":"q://1","current_item":null}"#
|
||||
let queue = try decode(json)
|
||||
#expect(queue.currentItem == nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user