Surface the existing pin (keep-from-cull) and per-file delete actions as visible inline buttons on each offline cache row instead of context-menu-only: a star toggles protection from auto-cull (and restore-if-missing), a trash culls that file early. Aligns wording/icons to the star metaphor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
4.9 KiB
Swift
128 lines
4.9 KiB
Swift
import XCTest
|
|
@testable import TVAnarchyCore
|
|
|
|
/// Guards the adult-config schema and the load-modify-save contract. The bug this
|
|
/// exists for: a per-field save (`AppSettings(surfaceAdultOnHome:)`) silently
|
|
/// reset every OTHER field to its default — so toggling the adult gate wiped
|
|
/// `hoverPreviews`. The store is redirected via `TV_ANARCHY_STATE_DIR` so the
|
|
/// real settings file is never touched.
|
|
final class SettingsStoreTests: XCTestCase {
|
|
private var tmp: URL!
|
|
|
|
override func setUpWithError() throws {
|
|
tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
.appendingPathComponent("settings-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
|
setenv("TV_ANARCHY_STATE_DIR", tmp.path, 1)
|
|
}
|
|
override func tearDownWithError() throws {
|
|
unsetenv("TV_ANARCHY_STATE_DIR")
|
|
try? FileManager.default.removeItem(at: tmp)
|
|
}
|
|
|
|
func testDefaultsAreAllOff() {
|
|
let s = AppSettings()
|
|
XCTAssertFalse(s.pornFeature)
|
|
XCTAssertFalse(s.surfaceAdultOnHome)
|
|
XCTAssertFalse(s.switchToAdultOnlyHome)
|
|
XCTAssertFalse(s.hoverPreviews)
|
|
XCTAssertEqual(s.adultQueueCount, 25)
|
|
XCTAssertEqual(s.playbackMode, .stream)
|
|
XCTAssertEqual(s.adultPlaybackMode, .stream)
|
|
XCTAssertEqual(s.appTheme, .standard)
|
|
}
|
|
|
|
/// A sibling field must survive a single-field update (the clobber regression).
|
|
func testSingleFieldUpdateKeepsSiblings() {
|
|
var s = SettingsStore.load()
|
|
s.hoverPreviews = true
|
|
SettingsStore.save(s)
|
|
|
|
// Simulate the LibraryController setter path: reload, change one field, save.
|
|
var t = SettingsStore.load()
|
|
t.surfaceAdultOnHome = true
|
|
SettingsStore.save(t)
|
|
|
|
let final = SettingsStore.load()
|
|
XCTAssertTrue(final.surfaceAdultOnHome)
|
|
XCTAssertTrue(final.hoverPreviews, "hoverPreviews must survive a surfaceAdultOnHome write")
|
|
}
|
|
|
|
/// Tolerant decode: an old file missing the new keys loads with them defaulted.
|
|
func testTolerantDecodeOfLegacyFile() throws {
|
|
let url = tmp.appendingPathComponent("settings.json")
|
|
try #"{"surfaceAdultOnHome":true,"hoverPreviews":true}"#
|
|
.write(to: url, atomically: true, encoding: .utf8)
|
|
let s = SettingsStore.load()
|
|
XCTAssertTrue(s.surfaceAdultOnHome)
|
|
XCTAssertTrue(s.hoverPreviews)
|
|
XCTAssertFalse(s.pornFeature) // absent → default
|
|
XCTAssertFalse(s.switchToAdultOnlyHome) // absent → default
|
|
XCTAssertEqual(s.adultQueueCount, 25) // absent → default
|
|
XCTAssertEqual(s.playbackMode, .stream) // absent → default
|
|
}
|
|
|
|
func testPlaybackModePersists() {
|
|
var s = SettingsStore.load()
|
|
s.playbackMode = .offline
|
|
SettingsStore.save(s)
|
|
XCTAssertEqual(SettingsStore.load().playbackMode, .offline)
|
|
}
|
|
|
|
func testAdultPlaybackModePersists() {
|
|
var s = SettingsStore.load()
|
|
s.adultPlaybackMode = .offline
|
|
SettingsStore.save(s)
|
|
XCTAssertEqual(SettingsStore.load().adultPlaybackMode, .offline)
|
|
}
|
|
|
|
func testForwardVolumeKeysTolerantDecode() throws {
|
|
let url = tmp.appendingPathComponent("settings.json")
|
|
try #"{"forwardMediaKeys":false}"#.write(to: url, atomically: true, encoding: .utf8)
|
|
let s = SettingsStore.load()
|
|
XCTAssertFalse(s.forwardMediaKeys)
|
|
XCTAssertTrue(s.forwardVolumeKeys)
|
|
}
|
|
|
|
func testForwardVolumeKeysPersists() {
|
|
var s = SettingsStore.load()
|
|
s.forwardVolumeKeys = false
|
|
SettingsStore.save(s)
|
|
XCTAssertFalse(SettingsStore.load().forwardVolumeKeys)
|
|
}
|
|
|
|
func testAdultQueueCountPersists() {
|
|
var s = SettingsStore.load()
|
|
s.adultQueueCount = 40
|
|
SettingsStore.save(s)
|
|
XCTAssertEqual(SettingsStore.load().adultQueueCount, 40)
|
|
}
|
|
|
|
func testAppThemePersists() {
|
|
var s = SettingsStore.load()
|
|
s.appTheme = .winampClassic
|
|
SettingsStore.save(s)
|
|
XCTAssertEqual(SettingsStore.load().appTheme, .winampClassic)
|
|
}
|
|
|
|
func testAppThemeTolerantDecode() throws {
|
|
let url = tmp.appendingPathComponent("settings.json")
|
|
try #"{"appTheme":"winamp-modern"}"#.write(to: url, atomically: true, encoding: .utf8)
|
|
XCTAssertEqual(SettingsStore.load().appTheme, .winampModern)
|
|
}
|
|
|
|
@MainActor
|
|
func testLibraryControllerPatchSettingsKeepsSiblings() {
|
|
var s = SettingsStore.load()
|
|
s.hoverPreviews = true
|
|
SettingsStore.save(s)
|
|
|
|
let lib = LibraryController(watchHistory: WatchHistoryController())
|
|
lib.patchSettings(AppSettingsPatch(notifyDownloads: false))
|
|
|
|
let disk = SettingsStore.load()
|
|
XCTAssertFalse(disk.notifyDownloads)
|
|
XCTAssertTrue(disk.hoverPreviews)
|
|
XCTAssertTrue(lib.appSettings.hoverPreviews)
|
|
}
|
|
}
|