portico/Tests/PorticoTests/SceneBuilderTests.swift

233 lines
7.8 KiB
Swift

import Testing
@_spi(Portico) import Portico
@_spi(SGTKInternal) import Gtk
import Adw
@_silgen_name("g_main_context_iteration")
private nonisolated func scene_g_main_context_iteration(
_ context: UnsafeMutableRawPointer?, _ mayBlock: Int32
) -> Int32
/// Drives the GLib main context until `condition` holds or `turns` is exhausted.
/// Returns `true` if the loop exhausted turns without the condition becoming true.
@MainActor private func pump(until condition: () -> Bool, turns: Int = 200) -> Bool {
for _ in 0..<turns {
if condition() { return false }
_ = scene_g_main_context_iteration(nil, 0)
}
return true
}
@MainActor @Suite(.serialized) struct SceneBuilderTests {
// MARK: - Pure identity tests
@SceneBuilder private func eitherScene(_ flag: Bool) -> some Scene {
if flag {
ApplicationWindow { _ in Gtk.Label(str: "first") }
.title("A")
} else {
ApplicationWindow { _ in Gtk.Label(str: "second") }
.title("B")
}
}
@Test func ifElseSelectsFirstBranch() {
let scene = eitherScene(true)
#expect((scene as? MountableScene)?.identity == .first(.leaf))
}
@Test func ifElseSelectsSecondBranch() {
let scene = eitherScene(false)
#expect((scene as? MountableScene)?.identity == .second(.leaf))
}
@SceneBuilder private func bareIfScene(_ flag: Bool) -> some Scene {
if flag {
ApplicationWindow { _ in Gtk.Label(str: "present") }
.title("Present")
}
}
@Test func bareIfIdentityPresent() {
let scene = bareIfScene(true)
#expect((scene as? MountableScene)?.identity == .present(.leaf))
}
@Test func bareIfIdentityEmpty() {
let scene = bareIfScene(false)
#expect((scene as? MountableScene)?.identity == .empty)
}
@SceneBuilder private func nestedScene(_ level: Int) -> some Scene {
if level == 0 {
ApplicationWindow { _ in Gtk.Label(str: "zero") }
.title("Zero")
} else if level == 1 {
ApplicationWindow { _ in Gtk.Label(str: "one") }
.title("One")
} else {
ApplicationWindow { _ in Gtk.Label(str: "other") }
.title("Other")
}
}
@Test func nestedElseIfIdentity() {
// Swift result builders handle `else if` chains by nesting the first
// two arms buildEither(first: buildEither(first: A, second: B),
// second: C) producing EitherScene<EitherScene<A, B>, C> (SE-0289).
// Level 0 first(.first(.leaf)) = [0, 0].
#expect(
(nestedScene(0) as? MountableScene)?.identity
== .first(.first(.leaf))
)
// Level 1 first(.second(.leaf)) = [0, 1].
#expect(
(nestedScene(1) as? MountableScene)?.identity
== .first(.second(.leaf))
)
// Level 2 second(.leaf) = [1].
#expect(
(nestedScene(2) as? MountableScene)?.identity
== .second(.leaf)
)
}
// MARK: - Live swap tests (require GTK + Adw)
@Test func hostSwapsWindowOnBranchFlip() {
guard Gtk.initCheck() else { return }
let box = StateBox(false)
let app = Adw.Application(applicationId: nil, flags: .defaultFlags)
@SceneBuilder func makeScene() -> some Scene {
if Binding(box).wrappedValue {
ApplicationWindow { _ in Gtk.Label(str: "main") }
.title("Main")
.defaultSize(width: 1080, height: 720)
} else {
ApplicationWindow { _ in Gtk.Label(str: "onboard") }
.title("Onboarding")
.defaultSize(width: 800, height: 600)
}
}
let host = SceneHost(app: app, makeScene: { makeScene() })
host.start()
#expect(host.liveIdentity == .second(.leaf))
#expect(host.live?.window.getTitle() == "Onboarding")
let old = host.live!
box.set(true)
let timedOut = pump(until: { host.liveIdentity == .first(.leaf) })
#expect(!timedOut, "pump timed out waiting for branch swap")
#expect(host.live?.window.getTitle() == "Main")
#expect(old.isDismantled)
#expect(host.live !== old)
}
@Test func swapTearsDownOldSubtree() {
guard Gtk.initCheck() else { return }
let branchBox = StateBox(false)
let titleBox = StateBox(0)
let app = Adw.Application(applicationId: nil, flags: .defaultFlags)
@SceneBuilder func makeScene() -> some Scene {
if Binding(branchBox).wrappedValue {
ApplicationWindow { _ in Gtk.Label(str: "main window") }
.title("Main")
} else {
ApplicationWindow { _ in
Portico.Label(str: nil).label("v=\(titleBox.get())")
}
.title("Onboarding")
}
}
let host = SceneHost(app: app, makeScene: { makeScene() })
host.start()
let old = host.live!
// Capture the mounted label before the swap.
let oldLabel = old.window.getChild() as? Gtk.Label
let textBefore = oldLabel?.getText()
branchBox.set(true)
let timedOut = pump(until: { host.liveIdentity == .first(.leaf) })
#expect(!timedOut, "pump timed out waiting for branch swap")
// After teardown, setting titleBox should NOT update the old label.
titleBox.set(99)
// Pump a few turns so any stray updates would fire.
_ = pump(until: { false }, turns: 10)
// The old label text must still be what it was before the swap.
#expect(oldLabel?.getText() == textBefore,
"Old label text changed after teardown: was '\(textBefore ?? "nil")', now '\(oldLabel?.getText() ?? "nil")'")
}
@Test func identicalIdentityDoesNotRebuild() {
guard Gtk.initCheck() else { return }
let box = StateBox("Initial")
let app = Adw.Application(applicationId: nil, flags: .defaultFlags)
@SceneBuilder func makeScene() -> some Scene {
ApplicationWindow { _ in Gtk.Label(str: "fixed content") }
.title(Binding(box).wrappedValue)
}
let host = SceneHost(app: app, makeScene: { makeScene() })
host.start()
let before = host.live!
box.set("Changed")
_ = pump(until: { false }, turns: 50)
#expect(host.live === before, "Window was rebuilt when only title changed")
}
@Test func falseBareIfAttachesNoWindow() {
guard Gtk.initCheck() else { return }
let box = StateBox(true)
let app = Adw.Application(applicationId: nil, flags: .defaultFlags)
@SceneBuilder func makeScene() -> some Scene {
if Binding(box).wrappedValue {
ApplicationWindow { _ in Gtk.Label(str: "visible") }
}
}
let host = SceneHost(app: app, makeScene: { makeScene() })
host.start()
#expect(host.live != nil)
let old = host.live!
box.set(false)
let timedOut = pump(until: { host.liveIdentity == .empty })
#expect(!timedOut, "pump timed out waiting for empty branch")
#expect(host.live == nil)
#expect(old.isDismantled)
}
@Test func secondStartRepresentsLiveWindow() {
guard Gtk.initCheck() else { return }
let app = Adw.Application(applicationId: nil, flags: .defaultFlags)
@SceneBuilder func makeScene() -> some Scene {
ApplicationWindow { _ in Gtk.Label(str: "single") }
.title("Single")
}
let host = SceneHost(app: app, makeScene: { makeScene() })
host.start()
let first = host.live!
host.start()
#expect(host.live === first, "Second start() created a new window")
}
}