tv-anarchy/Tests/TVAnarchyCoreTests/SleepTimerTests.swift
Natalie 92b38b1bae refactor(tv-anarchy): rename PlumTV→TVAnarchy and land session work
Renames Sources/PlumTV→TVAnarchy and PlumTVCore→TVAnarchyCore (the rename
the auto-commit service couldn't stage — it git-add'd the old, now-gone
paths and aborted every cycle), and commits the accumulated work:

- Library: black-built index fast path (LibraryIndex + scanFromIndex) with
  NFS-walk fallback; incremental --add on download-complete; mtime staleness
  gate; loose-file series-collapse fix; determinate scan/index progress.
- Cover art: keyless TVmaze cartoon-vs-live-action disambiguation (type/year).
- Player: sleep timer (timed + end-of-episode); visibility-gated polling.
- Home: Continue Watching cover art + live refresh; Recently Added; adult gate.
- Logs: multi-line selection + copy; truncated giant tx-list errors.
- Hover previews (opt-in) via black ffmpeg + scp.

Also gitignores foreign project trees (governor/mcp/fleet/recommender) that
sit in this directory but belong to their own repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:04:22 -07:00

72 lines
2.6 KiB
Swift

import XCTest
@testable import TVAnarchyCore
/// The player's sleep timer: arming a preset sets a fire date ~N minutes out;
/// end-of-episode arms without a fire date; cancel clears it.
final class SleepTimerTests: XCTestCase {
@MainActor
func testTimedArmSetsFireDate() {
let p = PlayerController()
XCTAssertFalse(p.sleepArmed)
XCTAssertNil(p.sleepFiresAt)
p.setSleepTimer(minutes: 30)
XCTAssertTrue(p.sleepArmed)
let secs = try? XCTUnwrap(p.sleepFiresAt).timeIntervalSinceNow
XCTAssertNotNil(secs)
XCTAssertTrue((29 * 60)...(30 * 60) ~= (secs ?? 0), "≈30 min ahead, got \(secs ?? -1)")
p.cancelSleep()
XCTAssertFalse(p.sleepArmed)
XCTAssertEqual(p.sleep, .off)
XCTAssertNil(p.sleepFiresAt)
}
@MainActor
func testEndOfEpisodeHasNoFireDate() {
let p = PlayerController()
p.setSleepAtEpisodeEnd()
XCTAssertEqual(p.sleep, .endOfEpisode)
XCTAssertTrue(p.sleepArmed)
XCTAssertNil(p.sleepFiresAt, "end-of-episode fires on item end, not a clock time")
}
@MainActor
func testZeroMinutesIsOff() {
let p = PlayerController()
p.setSleepTimer(minutes: 0)
XCTAssertFalse(p.sleepArmed)
}
// MARK: end-of-episode firing decision (the bug: it never fired)
private func status(title: String? = nil, pos: Int? = nil,
position: Double? = nil, duration: Double? = nil) -> PlaybackStatus {
PlaybackStatus(playing: true, title: title, position: position,
duration: duration, playlistPos: pos)
}
func testFiresWhenPlaylistAdvances() {
XCTAssertTrue(PlayerController.shouldFireEndOfEpisode(
armedTitle: "S01E15", armedPos: 2, status: status(title: "S01E15", pos: 3)))
}
func testFiresWhenTitleChanges_noPlaylistPos() {
XCTAssertTrue(PlayerController.shouldFireEndOfEpisode(
armedTitle: "Ep A", armedPos: nil, status: status(title: "Ep B")))
}
func testFiresAtEndOfLastItem() {
// Last episode: nothing advances, title unchanged must still fire when
// the item reaches its end (this is what was dead before for mpv).
XCTAssertTrue(PlayerController.shouldFireEndOfEpisode(
armedTitle: "Finale", armedPos: 12,
status: status(title: "Finale", pos: 12, position: 2599, duration: 2600)))
}
func testDoesNotFireMidEpisode() {
XCTAssertFalse(PlayerController.shouldFireEndOfEpisode(
armedTitle: "S01E15", armedPos: 2,
status: status(title: "S01E15", pos: 2, position: 600, duration: 2600)))
}
}