luminate/Tests/LuminateUITests/PreferenceTests.swift

172 lines
6.4 KiB
Swift

//
// PreferenceTests.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Foundation
@_spi(SGTKInternal) import Gtk
import LuminateCore
@_spi(Portico) import Portico
import Testing
@testable import LuminateUI
/// Test view that renders the current server URL preference.
private struct PreferenceLabel: View {
@Preference(.serverURL) private var serverURL: URL?
var body: some View {
Label(str: "").label { serverURL?.absoluteString ?? "No server configured" }
}
}
/// Records each value a tracked closure observes, so a test can assert re-evaluation.
private final class SessionRecorder {
var values: [Bool] = []
}
/// Exercises preference mounting, observation, and persistence wiring.
@Suite(.serialized) @MainActor struct PreferenceTests {
@Test("An unset preference reads nil at mount")
func unsetAtMount() {
guard Gtk.initCheck() else { return }
let preferences = Preferences(store: EphemeralPreferenceStore())
let context = MountContext()
let label =
AnyView(PreferenceLabel().environment(\.preferences, preferences))
.makeWidget(context) as! Gtk.Label
defer { context.registry.teardown() }
#expect(label.getText() == "No server configured")
}
@Test("An un-injected preference resolves through the shared coordinator")
func resolvesWithoutInjection() {
guard Gtk.initCheck() else { return }
Preferences.shared[.serverURL] = URL(string: "http://shared.test")
defer { Preferences.shared[.serverURL] = nil }
let context = MountContext()
let label = AnyView(PreferenceLabel()).makeWidget(context) as! Gtk.Label
defer { context.registry.teardown() }
#expect(label.getText() == "http://shared.test")
}
@Test("Writing a preference updates the same mounted label")
func liveUpdate() {
guard Gtk.initCheck() else { return }
let preferences = Preferences(store: EphemeralPreferenceStore())
let context = MountContext()
let label =
AnyView(PreferenceLabel().environment(\.preferences, preferences))
.makeWidget(context) as! Gtk.Label
defer { context.registry.teardown() }
let pointer = label.pointer
preferences.slot(for: .serverURL).value = URL(string: "http://jellyfin.test:8096")
pump {
label.getText() == "http://jellyfin.test:8096"
}
#expect(label.pointer == pointer)
#expect(label.getText() == "http://jellyfin.test:8096")
}
@Test("A preference write reaches the in-memory backend after flush")
func persistsToBackend() async throws {
let store = EphemeralPreferenceStore()
let preferences = Preferences(store: store)
preferences.slot(for: .serverURL).value = URL(string: "http://jellyfin.test:8096")
try await store.flush()
#expect(await store.storedValues["server.url"] == .text("http://jellyfin.test:8096"))
}
@Test("A seeded store presents its value at mount")
func restoresAtMount() {
guard Gtk.initCheck() else { return }
let store = EphemeralPreferenceStore(
initialSnapshot: ["server.url": .text("http://seeded.test")]
)
let preferences = Preferences(store: store)
let context = MountContext()
let label =
AnyView(PreferenceLabel().environment(\.preferences, preferences))
.makeWidget(context) as! Gtk.Label
defer { context.registry.teardown() }
#expect(label.getText() == "http://seeded.test")
}
@Test("Setting a preference to nil enqueues a deletion")
func deletesFromBackend() async throws {
let store = EphemeralPreferenceStore(
initialSnapshot: ["server.url": .text("http://seeded.test")]
)
let preferences = Preferences(store: store)
preferences.slot(for: .serverURL).value = nil
try await store.flush()
#expect(await store.storedValues["server.url"] == nil)
}
@Test("A stored token reports an authenticated coordinator")
func authenticatedWithStoredToken() {
let store = EphemeralPreferenceStore(initialSnapshot: ["auth.accessToken": .text("tok")])
#expect(Preferences(store: store).isAuthenticated)
}
@Test("A missing or empty token reports an unauthenticated coordinator")
func unauthenticatedWithoutToken() {
let preferences = Preferences(store: EphemeralPreferenceStore())
#expect(preferences.isAuthenticated == false)
preferences.slot(for: .accessToken).value = ""
#expect(preferences.isAuthenticated == false)
}
@Test("Writing the access token re-evaluates a tracker that read the session state")
func authenticationDrivesTrackedReevaluation() {
guard Gtk.initCheck() else { return }
let preferences = Preferences(store: EphemeralPreferenceStore())
let recorder = SessionRecorder()
let tracker = DependencyTracker { recorder.values.append(preferences.isAuthenticated) }
tracker.run()
defer { tracker.teardown() }
preferences.slot(for: .accessToken).value = "tok"
pump { recorder.values.count > 1 }
#expect(recorder.values == [false, true])
}
private func pump(until condition: () -> Bool = { false }, turns: Int = 200) {
for _ in 0..<turns {
if condition() { return }
_ = preference_g_main_context_iteration(nil, 0)
}
}
}
// UPSTREAM: Portico should expose a main-loop pump for tests so suites do not need @_silgen_name.
@_silgen_name("g_main_context_iteration")
private nonisolated func preference_g_main_context_iteration(
_ context: UnsafeMutableRawPointer?,
_ mayBlock: Int32
) -> Int32