Files
yattee/YatteeTests/PlayerTests.swift
2026-02-08 18:33:56 +01:00

372 lines
11 KiB
Swift

//
// PlayerTests.swift
// YatteeTests
//
// Tests for player service and SponsorBlock integration.
//
import Testing
import Foundation
@testable import Yattee
// MARK: - SponsorBlock Category Extended Tests
@Suite("SponsorBlock Category Extended Tests")
struct SponsorBlockCategoryExtendedTests {
@Test("All categories have descriptions")
func descriptions() {
for category in SponsorBlockCategory.allCases {
#expect(!category.localizedDescription.isEmpty)
}
}
@Test("Default auto-skip categories")
func defaultAutoSkip() {
#expect(SponsorBlockCategory.sponsor.defaultAutoSkip)
#expect(SponsorBlockCategory.selfpromo.defaultAutoSkip)
#expect(SponsorBlockCategory.interaction.defaultAutoSkip)
#expect(!SponsorBlockCategory.filler.defaultAutoSkip)
#expect(!SponsorBlockCategory.highlight.defaultAutoSkip)
}
@Test("Highlight category exists")
func highlightCategory() {
#expect(SponsorBlockCategory.highlight.rawValue == "poi_highlight")
#expect(SponsorBlockCategory.highlight.displayName == "Highlight")
}
}
// MARK: - SponsorBlock Segment Tests
@Suite("SponsorBlock Segment Tests")
struct SponsorBlockSegmentTests {
@Test("Segment timing calculations")
func segmentTiming() throws {
let json = """
{
"UUID": "test-uuid",
"category": "sponsor",
"actionType": "skip",
"segment": [10.5, 30.0],
"videoDuration": 600.0,
"votes": 10,
"description": "Sponsor segment"
}
"""
let segment = try JSONDecoder().decode(SponsorBlockSegment.self, from: json.data(using: .utf8)!)
#expect(segment.uuid == "test-uuid")
#expect(segment.startTime == 10.5)
#expect(segment.endTime == 30.0)
#expect(segment.duration == 19.5)
#expect(segment.category == .sponsor)
#expect(segment.actionType == .skip)
#expect(!segment.isPointOfInterest)
#expect(segment.segmentDescription == "Sponsor segment")
}
@Test("Point of interest detection")
func pointOfInterest() throws {
let json = """
{
"UUID": "poi-uuid",
"category": "poi_highlight",
"actionType": "poi",
"segment": [120.0, 120.0]
}
"""
let segment = try JSONDecoder().decode(SponsorBlockSegment.self, from: json.data(using: .utf8)!)
#expect(segment.isPointOfInterest)
#expect(segment.startTime == segment.endTime)
}
}
// MARK: - Segment Array Extension Tests
@Suite("Segment Array Extensions")
struct SegmentArrayTests {
let segments: [SponsorBlockSegment]
init() throws {
let json = """
[
{"UUID": "1", "category": "sponsor", "actionType": "skip", "segment": [10.0, 20.0]},
{"UUID": "2", "category": "intro", "actionType": "skip", "segment": [0.0, 5.0]},
{"UUID": "3", "category": "selfpromo", "actionType": "mute", "segment": [30.0, 40.0]},
{"UUID": "4", "category": "outro", "actionType": "skip", "segment": [580.0, 600.0]}
]
"""
self.segments = try JSONDecoder().decode([SponsorBlockSegment].self, from: json.data(using: .utf8)!)
}
@Test("Filter skippable segments")
func skippable() {
let skippable = segments.skippable()
#expect(skippable.count == 3)
#expect(!skippable.contains { $0.uuid == "3" }) // mute action excluded
}
@Test("Filter by categories")
func inCategories() {
let sponsorOnly = segments.inCategories([.sponsor])
#expect(sponsorOnly.count == 1)
#expect(sponsorOnly.first?.uuid == "1")
let multiple = segments.inCategories([.sponsor, .intro])
#expect(multiple.count == 2)
}
@Test("Find segment at time")
func segmentAtTime() {
let atStart = segments.segment(at: 2.0)
#expect(atStart?.uuid == "2") // intro 0-5
let atSponsor = segments.segment(at: 15.0)
#expect(atSponsor?.uuid == "1") // sponsor 10-20
let atNothing = segments.segment(at: 25.0)
#expect(atNothing == nil)
}
@Test("Find next segment after time")
func nextSegmentAfterTime() {
let afterStart = segments.nextSegment(after: 6.0)
#expect(afterStart?.uuid == "1") // sponsor at 10.0
let afterSponsor = segments.nextSegment(after: 25.0)
#expect(afterSponsor?.uuid == "3") // selfpromo at 30.0
let afterAll = segments.nextSegment(after: 590.0)
#expect(afterAll == nil)
}
}
// MARK: - Player State Tests
@Suite("Player State Tests")
@MainActor
struct PlayerStateTests {
@Test("Initial state")
func initialState() {
let state = PlayerState()
#expect(state.playbackState == .idle)
#expect(state.currentVideo == nil)
#expect(state.currentTime == 0)
#expect(state.duration == 0)
#expect(state.rate == .x1)
#expect(!state.isMuted)
}
@Test("Progress calculation")
func progressCalculation() {
let state = PlayerState()
state.duration = 100
state.currentTime = 50
#expect(state.progress == 0.5)
}
@Test("Progress calculation with zero duration")
func progressZeroDuration() {
let state = PlayerState()
state.duration = 0
state.currentTime = 50
#expect(state.progress == 0)
}
@Test("Time formatting")
func timeFormatting() {
let state = PlayerState()
state.currentTime = 65 // 1:05
#expect(state.formattedCurrentTime == "1:05")
state.duration = 3661 // 1:01:01
#expect(state.formattedDuration == "1:01:01")
state.currentTime = 3600 // remaining = 61 seconds = 1:01
#expect(state.formattedRemainingTime == "-1:01")
}
@Test("Queue operations")
func queueOperations() {
let state = PlayerState()
let video1 = Video(
id: .global("video1"),
title: "Video 1",
description: nil,
author: Author(id: "ch1", name: "Channel"),
duration: 100,
publishedAt: nil,
publishedText: nil,
viewCount: nil,
likeCount: nil,
thumbnails: [],
isLive: false,
isUpcoming: false,
scheduledStartTime: nil
)
let video2 = Video(
id: .global("video2"),
title: "Video 2",
description: nil,
author: Author(id: "ch1", name: "Channel"),
duration: 200,
publishedAt: nil,
publishedText: nil,
viewCount: nil,
likeCount: nil,
thumbnails: [],
isLive: false,
isUpcoming: false,
scheduledStartTime: nil
)
// Add to queue
state.addToQueue(video1)
state.addToQueue(video2)
#expect(state.queue.count == 2)
#expect(!state.hasPrevious)
#expect(state.hasNext)
// Advance - returns first item (video1) and removes it from queue
let next = state.advanceQueue()
#expect(next?.video.id == video1.id)
#expect(state.queue.count == 1)
#expect(!state.hasPrevious) // No history yet
#expect(state.hasNext) // video2 still in queue
// Add video1 to history manually (simulating playback)
state.pushToHistory(next!)
// Now advance to video2
let next2 = state.advanceQueue()
#expect(next2?.video.id == video2.id)
#expect(state.hasPrevious) // video1 is in history
#expect(!state.hasNext) // queue is empty
// Retreat - returns last item from history (video1)
let prev = state.retreatQueue()
#expect(prev?.video.id == video1.id)
#expect(!state.hasPrevious) // history is now empty
// Clear
state.clearQueue()
#expect(state.queue.isEmpty)
}
@Test("SponsorBlock auto-skip categories")
func autoSkipCategories() {
let state = PlayerState()
// Default should include common skip categories
#expect(state.autoSkipCategories.contains(.sponsor))
#expect(state.autoSkipCategories.contains(.selfpromo))
#expect(!state.autoSkipCategories.contains(.filler))
}
}
// MARK: - Playback Rate Tests
@Suite("Playback Rate Tests")
struct PlaybackRateTests {
@Test("All rates have display text")
func displayText() {
for rate in PlaybackRate.allCases {
#expect(!rate.displayText.isEmpty)
}
}
@Test("Normal rate displays correctly")
func normalRate() {
#expect(PlaybackRate.x1.displayText == "Normal")
}
@Test("Other rates format correctly")
func otherRates() {
#expect(PlaybackRate.x15.displayText == "1.5x")
#expect(PlaybackRate.x2.displayText == "2x")
#expect(PlaybackRate.x025.displayText == "0.25x")
}
@Test("Compact display text always shows numeric format")
func compactDisplayText() {
#expect(PlaybackRate.x1.compactDisplayText == "1x")
#expect(PlaybackRate.x15.compactDisplayText == "1.5x")
#expect(PlaybackRate.x2.compactDisplayText == "2x")
}
}
// MARK: - Video Chapter Tests
@Suite("Video Chapter Tests")
struct VideoChapterTests {
@Test("Chapter initialization")
func initialization() {
let chapter = VideoChapter(
title: "Introduction",
startTime: 0,
endTime: 60
)
#expect(chapter.title == "Introduction")
#expect(chapter.startTime == 0)
#expect(chapter.endTime == 60)
#expect(chapter.duration == 60)
}
@Test("Formatted start time")
func formattedStartTime() {
let chapter1 = VideoChapter(title: "A", startTime: 65)
#expect(chapter1.formattedStartTime == "1:05")
let chapter2 = VideoChapter(title: "B", startTime: 3661)
#expect(chapter2.formattedStartTime == "1:01:01")
}
@Test("Current chapter detection")
@MainActor
func currentChapter() {
let state = PlayerState()
state.chapters = [
VideoChapter(title: "Intro", startTime: 0, endTime: 30),
VideoChapter(title: "Main", startTime: 30, endTime: 120),
VideoChapter(title: "Outro", startTime: 120, endTime: 150)
]
state.currentTime = 15
#expect(state.currentChapter?.title == "Intro")
state.currentTime = 60
#expect(state.currentChapter?.title == "Main")
state.currentTime = 130
#expect(state.currentChapter?.title == "Outro")
}
}
// MARK: - Playback State Tests
@Suite("Playback State Tests")
struct PlaybackStateTests {
@Test("State equality")
func stateEquality() {
#expect(PlaybackState.idle == PlaybackState.idle)
#expect(PlaybackState.playing == PlaybackState.playing)
#expect(PlaybackState.idle != PlaybackState.playing)
// Failed states are equal regardless of error content
let error1 = NSError(domain: "test", code: 1)
let error2 = NSError(domain: "test", code: 2)
#expect(PlaybackState.failed(error1) == PlaybackState.failed(error2))
}
}