233 lines
7.8 KiB
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")
|
|
}
|
|
}
|