188 lines
6.4 KiB
Swift
188 lines
6.4 KiB
Swift
import Testing
|
|
@_spi(SGTKInternal) import Gtk
|
|
import Adw
|
|
@_spi(Portico) import Portico
|
|
|
|
@MainActor @Suite(.serialized) struct TwoWayBindingTests {
|
|
|
|
// MARK: - SwitchRow active
|
|
|
|
/// Adw.SwitchRow.active propagates both directions via notify.
|
|
@Test func switchRowActiveTwoWay() {
|
|
guard Gtk.initCheck() else { return }
|
|
let box = StateBox(false)
|
|
let context = MountContext()
|
|
let view = AnyView(SwitchRow().active(Binding(box)))
|
|
let row = view.makeWidget(context) as! Adw.SwitchRow
|
|
#expect(row.getActive() == false)
|
|
box.set(true)
|
|
#expect(row.getActive() == true)
|
|
row.setActive(isActive: false)
|
|
#expect(box.peek() == false)
|
|
}
|
|
|
|
@Test func detailedNotifyDoesNotCrossProperties() {
|
|
guard Gtk.initCheck() else { return }
|
|
let context = MountContext()
|
|
let widget = Gtk.Switch()
|
|
let active = StateBox(false)
|
|
let sensitive = StateBox(true)
|
|
var activeSetCount = 0
|
|
var sensitiveSetCount = 0
|
|
let activeBinding = Binding<Bool>(
|
|
get: { active.peek() },
|
|
set: { value in
|
|
activeSetCount += 1
|
|
active.set(value)
|
|
}
|
|
)
|
|
let sensitiveBinding = Binding<Bool>(
|
|
get: { sensitive.peek() },
|
|
set: { value in
|
|
sensitiveSetCount += 1
|
|
sensitive.set(value)
|
|
}
|
|
)
|
|
|
|
Portico.bindProperty(
|
|
widget,
|
|
activeBinding,
|
|
registry: context.registry,
|
|
notifyDetail: "active",
|
|
read: { widget.getActive() },
|
|
write: { widget.setActive(isActive: $0) }
|
|
)
|
|
Portico.bindProperty(
|
|
widget,
|
|
sensitiveBinding,
|
|
registry: context.registry,
|
|
notifyDetail: "sensitive",
|
|
read: { widget.getSensitive() },
|
|
write: { widget.setSensitive(sensitive: $0) }
|
|
)
|
|
|
|
// Keep active out of sync without sending a widget notification. A
|
|
// broadcast handler would now observe active=true and invoke its setter
|
|
// when the unrelated sensitive property changes.
|
|
widget.setActive(isActive: true)
|
|
active.set(false)
|
|
activeSetCount = 0
|
|
sensitiveSetCount = 0
|
|
|
|
widget.setSensitive(sensitive: false)
|
|
|
|
#expect(activeSetCount == 0)
|
|
#expect(sensitiveSetCount == 1)
|
|
#expect(active.peek() == false)
|
|
#expect(sensitive.peek() == false)
|
|
}
|
|
|
|
@Test func unresolvableDetailFallsBackToBroadcast() {
|
|
guard Gtk.initCheck() else { return }
|
|
let box = StateBox(false)
|
|
let context = MountContext()
|
|
let widget = Gtk.Switch()
|
|
Portico.bindProperty(
|
|
widget,
|
|
Binding(box),
|
|
registry: context.registry,
|
|
notifyDetail: "not-a-real-property",
|
|
read: { widget.getActive() },
|
|
write: { widget.setActive(isActive: $0) }
|
|
)
|
|
|
|
widget.setActive(isActive: true)
|
|
|
|
#expect(box.peek() == true)
|
|
}
|
|
|
|
// MARK: - SpinRow value
|
|
|
|
/// Adw.SpinRow.value propagates both directions via notify.
|
|
@Test func spinRowValueTwoWay() {
|
|
guard Gtk.initCheck() else { return }
|
|
let box = StateBox(10.0)
|
|
let context = MountContext()
|
|
let view = AnyView(SpinRow(min: 0, max: 100, step: 5).value(Binding(box)))
|
|
let row = view.makeWidget(context) as! Adw.SpinRow
|
|
#expect(row.getValue() == 10.0)
|
|
box.set(42.0)
|
|
#expect(row.getValue() == 42.0)
|
|
row.setValue(value: 20.0)
|
|
#expect(box.peek() == 20.0)
|
|
}
|
|
|
|
// MARK: - Enum property two-way
|
|
|
|
/// Gtk.Widget.halign (Gtk.Align enum) binds two-way.
|
|
@Test func enumPropertyTwoWay() {
|
|
guard Gtk.initCheck() else { return }
|
|
let box = StateBox(Gtk.Align.start)
|
|
let context = MountContext()
|
|
let view = AnyView(Label(str: "x").halign(Binding(box)))
|
|
let w = view.makeWidget(context)
|
|
#expect(w.getHalign() == Gtk.Align.start)
|
|
box.set(.end)
|
|
#expect(w.getHalign() == .end)
|
|
w.setHalign(align: .center)
|
|
#expect(box.peek() == .center)
|
|
}
|
|
|
|
// MARK: - Lifted optional skips nil write-back
|
|
|
|
/// The lifted Binding<String> overload on String? properties never writes
|
|
/// nil back into the binding.
|
|
@Test func liftedOptionalSkipsNil() {
|
|
guard Gtk.initCheck() else { return }
|
|
let box = StateBox("A")
|
|
let context = MountContext()
|
|
let view = AnyView(Portico.Window().title(Binding(box)))
|
|
let win = view.makeWidget(context) as! Adw.Window
|
|
#expect(win.getTitle() == "A")
|
|
box.set("B")
|
|
#expect(win.getTitle() == "B")
|
|
win.setTitle(title: "C")
|
|
#expect(box.peek() == "C")
|
|
win.setTitle(title: nil)
|
|
#expect(box.peek() == "C")
|
|
}
|
|
|
|
// MARK: - Non-Equatable stays one-way
|
|
|
|
/// Gtk.Adjustment is not Equatable, so the binding is one-way only.
|
|
@Test func nonEquatableStaysOneWay() {
|
|
guard Gtk.initCheck() else { return }
|
|
let initial = Gtk.Adjustment(value: 0, lower: 0, upper: 100, stepIncrement: 1, pageIncrement: 10, pageSize: 0)
|
|
let other = Gtk.Adjustment(value: 50, lower: 0, upper: 100, stepIncrement: 1, pageIncrement: 10, pageSize: 0)
|
|
let box = StateBox(initial)
|
|
let context = MountContext()
|
|
let view = AnyView(SpinButton(adjustment: nil, climbRate: 0, digits: 0).adjustment(Binding(box)))
|
|
let sb = view.makeWidget(context) as! Gtk.SpinButton
|
|
box.set(other)
|
|
#expect(sb.getAdjustment().getValue() == 50.0)
|
|
let third = Gtk.Adjustment(value: 75, lower: 0, upper: 100, stepIncrement: 1, pageIncrement: 10, pageSize: 0)
|
|
sb.setAdjustment(adjustment: third)
|
|
#expect(box.peek() === other)
|
|
}
|
|
|
|
// MARK: - Teardown stops write-back
|
|
|
|
/// Tearing down the registry disconnects notify, so widget changes
|
|
/// no longer reach the binding.
|
|
@Test func teardownStopsWriteBack() {
|
|
guard Gtk.initCheck() else { return }
|
|
let box = StateBox(false)
|
|
let ctx = MountContext()
|
|
let view = AnyView(
|
|
SwitchRow().active(Binding(box))
|
|
)
|
|
let row = view.makeWidget(ctx) as! Adw.SwitchRow
|
|
#expect(row.getActive() == false)
|
|
|
|
ctx.registry.teardown()
|
|
|
|
// After teardown, widget change should not propagate
|
|
row.setActive(isActive: true)
|
|
#expect(box.peek() == false)
|
|
}
|
|
}
|