import Foundation import Testing import Adw import Gio @_spi(SGTKInternal) import Gtk @_spi(Portico) import Portico @_silgen_name("g_main_context_iteration") private nonisolated func nav_g_main_context_iteration( _ context: UnsafeMutableRawPointer?, _ mayBlock: Int32 ) -> Int32 /// Drives the GLib main context until `condition` holds or `turns` is exhausted. @MainActor private func pump(until condition: () -> Bool, turns: Int = 200) { for _ in 0.. Int { Int(nav.navigationStack.getNItems()) } // MARK: - Probe types /// A label that records how many times it was mounted per key. private final class MountCounter { var counts: [String: Int] = [:] func inc(_ key: String) { counts[key, default: 0] += 1 } } private struct ProbePage: View { let key: String let counter: MountCounter var body: Never { fatalError() } init(key: String, counter: MountCounter) { self.key = key; self.counter = counter } } @_spi(Portico) extension ProbePage: Mountable { @_spi(Portico) public func mount(_ ctx: MountContext) -> Gtk.Widget { counter.inc(key) return Gtk.Label(str: key) } } /// A shared mutable dictionary for tracking per-key subscription fires. private final class FireCounter { var fires: [String: Int] = [:] func inc(_ key: String) { fires[key, default: 0] += 1 } } /// A page that subscribes to a StateBox so we can verify the subscription /// is cancelled on page removal. private struct SubscribingPage: View { let key: String let box: StateBox let counter: FireCounter var body: Never { fatalError() } init(key: String, box: StateBox, counter: FireCounter) { self.key = key; self.box = box; self.counter = counter } } @_spi(Portico) extension SubscribingPage: Mountable { @_spi(Portico) public func mount(_ ctx: MountContext) -> Gtk.Widget { ctx.registry.add(box.subscribe { [key, counter] _ in counter.inc(key) }) return Gtk.Label(str: "sub \(key)") } } // MARK: - Tests @MainActor @Suite(.serialized) struct NavigationViewPathTests { // MARK: Static init @Test func staticInitMountsRootOnly() { guard Gtk.initCheck() else { return } let ctx = MountContext() let view = Portico.NavigationView { Label(str: "root") } let w = AnyView(view).makeWidget(ctx) as! Adw.NavigationView #expect(depth(w) == 1) #expect(w.getVisiblePage()?.getTag() == "portico.nav.root") #expect(ctx.registry.isEmpty == true) } @Test func generatedNoArgInitStillMountsEmpty() { guard Gtk.initCheck() else { return } let ctx = MountContext() let view = Portico.NavigationView() let w = AnyView(view).makeWidget(ctx) as! Adw.NavigationView #expect(depth(w) == 0) #expect(ctx.registry.isEmpty == true) } // MARK: Push / pop @Test func appendPushesDestinationPage() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let counter = MountCounter() let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in ProbePage(key: park.name, counter: counter) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView #expect(depth(w) == 1) box.set([Park(name: "Yosemite")]) #expect(depth(w) == 2) #expect(counter.counts["Yosemite"] == 1) } @Test func truncatePopsPage() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let counter = MountCounter() let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in ProbePage(key: park.name, counter: counter) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView box.set([Park(name: "Yosemite"), Park(name: "Zion")]) #expect(depth(w) == 3) box.set([Park(name: "Yosemite")]) #expect(depth(w) == 2) } @Test func commonPrefixSurvivesAppend() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let counter = MountCounter() let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in ProbePage(key: park.name, counter: counter) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView box.set([Park(name: "A")]) #expect(counter.counts["A"] == 1) box.set([Park(name: "A"), Park(name: "B")]) #expect(depth(w) == 3) #expect(counter.counts["A"] == 1) #expect(counter.counts["B"] == 1) } @Test func divergenceReplacesTailAndKeepsPrefix() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let counter = MountCounter() let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in ProbePage(key: park.name, counter: counter) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView box.set([Park(name: "A"), Park(name: "B")]) #expect(counter.counts["A"] == 1) #expect(counter.counts["B"] == 1) box.set([Park(name: "A"), Park(name: "C")]) #expect(depth(w) == 3) #expect(counter.counts["A"] == 1) #expect(counter.counts["C"] == 1) } // MARK: NavigationPath @Test func navigationPathRoutesByDynamicType() { guard Gtk.initCheck() else { return } let box = StateBox(NavigationPath()) let counter = MountCounter() let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in ProbePage(key: park.name, counter: counter) } .navigationDestination(for: City.self) { city in ProbePage(key: city.name, counter: counter) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView var path = NavigationPath() path.append(Park(name: "Yosemite")) path.append(City(name: "Portland")) box.set(path) #expect(depth(w) == 3) #expect(counter.counts["Yosemite"] == 1) #expect(counter.counts["Portland"] == 1) } // MARK: Widget pop @Test func widgetPopWritesBackToPath() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in Label(str: park.name) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView box.set([Park(name: "Yosemite"), Park(name: "Zion")]) #expect(depth(w) == 3) _ = w.pop() pump(until: { box.get().count == 1 }) #expect(box.get().count == 1) #expect(depth(w) == 2) } // MARK: Teardown @Test func poppedPageTearsDownSubscriptions() { guard Gtk.initCheck() else { return } let pathBox = StateBox<[Park]>([]) let subBox = StateBox(0) let counter = FireCounter() let view = Portico.NavigationView(path: Binding(pathBox)) { Label(str: "root") .navigationDestination(for: Park.self) { park in SubscribingPage(key: park.name, box: subBox, counter: counter) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView pathBox.set([Park(name: "Yosemite")]) #expect(depth(w) == 2) subBox.set(1) #expect(counter.fires["Yosemite"] == 1) pathBox.set([]) subBox.set(2) // Teardown should have cancelled the subscription; no second fire. #expect(counter.fires["Yosemite"] == 1) } // MARK: Missing destination @Test func missingDestinationKeepsPathAligned() { guard Gtk.initCheck() else { return } let box = StateBox<[Unregistered]>([]) let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView box.set([Unregistered(id: 1)]) #expect(depth(w) == 2) box.set([]) #expect(depth(w) == 1) } // MARK: navigationTitle @Test func navigationTitleSetsPageTitle() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") .navigationDestination(for: Park.self) { park in Label(str: park.name).navigationTitle(park.name) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView box.set([Park(name: "Yosemite")]) pump(until: { w.getVisiblePage()?.getTitle() == "Yosemite" }) #expect(w.getVisiblePage()?.getTitle() == "Yosemite") } @Test func navigationTitleBindingUpdatesLive() { guard Gtk.initCheck() else { return } let pathBox = StateBox<[Park]>([]) let titleBox = StateBox("Initial") let view = Portico.NavigationView(path: Binding(pathBox)) { Label(str: "root") .navigationDestination(for: Park.self) { park in Label(str: park.name).navigationTitle(Binding(titleBox)) } } let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView pathBox.set([Park(name: "Yosemite")]) pump(until: { w.getVisiblePage()?.getTitle() == "Initial" }, turns: 400) #expect(w.getVisiblePage()?.getTitle() == "Initial") titleBox.set("Renamed") pump(until: { w.getVisiblePage()?.getTitle() == "Renamed" }, turns: 400) #expect(w.getVisiblePage()?.getTitle() == "Renamed") } // MARK: Inherited modifier @Test func inheritedModifierStillApplies() { guard Gtk.initCheck() else { return } let box = StateBox<[Park]>([]) let view = Portico.NavigationView(path: Binding(box)) { Label(str: "root") }.popOnEscape(false) let w = AnyView(view).makeWidget(MountContext()) as! Adw.NavigationView #expect(w.getPopOnEscape() == false) } // MARK: Codable @Test func navigationPathCodableRoundTrip() throws { var path = NavigationPath() path.append(Park(name: "Yosemite")) path.append(City(name: "Portland")) let rep = try #require(path.codable) let data = try JSONEncoder().encode(rep) let decoded = try JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) let restored = NavigationPath(decoded) #expect(restored == path) } @Test func navigationPathCodableNilForNonCodableElement() { var path = NavigationPath() path.append(Unregistered(id: 1)) #expect(path.codable == nil) } }