@@ -64,7 +64,114 @@ final class SwiftCrossUITests: XCTestCase {
6464 return original == decoded
6565 }
6666
67+ func testStateObservation( ) {
68+ class NestedState : SwiftCrossUI . ObservableObject {
69+ @SwiftCrossUI . Published
70+ var count = 0
71+ }
72+
73+ class MyState : SwiftCrossUI . ObservableObject {
74+ @SwiftCrossUI . Published
75+ var count = 0
76+ @SwiftCrossUI . Published
77+ var publishedNestedState = NestedState ( )
78+ var unpublishedNestedState = NestedState ( )
79+ }
80+
81+ let state = MyState ( )
82+ var observedChange = false
83+ let cancellable = state. didChange. observe {
84+ observedChange = true
85+ }
86+
87+ // Ensures that published value type mutation triggers observation
88+ observedChange = false
89+ state. count += 1
90+ XCTAssert ( observedChange, " Expected value type mutation to trigger observation " )
91+
92+ // Ensure that published nested ObservableObject triggers observation
93+ observedChange = false
94+ state. publishedNestedState. count += 1
95+ XCTAssert ( observedChange, " Expected nested published observable object mutation to trigger observation " )
96+
97+ // Ensure that replacing published nested ObservableObject triggers observation
98+ observedChange = false
99+ state. publishedNestedState = NestedState ( )
100+ XCTAssert ( observedChange, " Expected replacing nested published observable object to trigger observation " )
101+
102+ // Ensure that replaced published nested ObservableObject triggers observation
103+ observedChange = false
104+ state. publishedNestedState. count += 1
105+ XCTAssert ( observedChange, " Expected replaced nested published observable object mutation to trigger observation " )
106+
107+ // Ensure that non-published nested ObservableObject doesn't trigger observation
108+ observedChange = false
109+ state. unpublishedNestedState. count += 1
110+ XCTAssert ( !observedChange, " Expected nested unpublished observable object mutation to not trigger observation " )
111+
112+ // Ensure that cancelling the observation prevents future observations
113+ cancellable. cancel ( )
114+ observedChange = false
115+ state. count += 1
116+ XCTAssert ( !observedChange, " Expected mutation not to trigger cancelled observation " )
117+ }
118+
67119 #if canImport(AppKitBackend)
120+ // TODO: Create mock backend so that this can be tested on all platforms. There's
121+ // nothing AppKit-specific about it.
122+ func testThrottledStateObservation( ) async {
123+ class MyState : SwiftCrossUI . ObservableObject {
124+ @SwiftCrossUI . Published
125+ var count = 0
126+ }
127+
128+ /// A thread-safe count.
129+ actor Count {
130+ var count = 0
131+
132+ func update( _ action: ( Int ) -> Int ) {
133+ count = action ( count)
134+ }
135+ }
136+
137+ // Number of mutations to perform
138+ let mutationCount = 20
139+ // Length of each fake state update
140+ let updateDuration = 0.02
141+ // Delay between observation-causing state mutations
142+ let mutationGap = 0.01
143+
144+ let state = MyState ( )
145+ let updateCount = Count ( )
146+
147+ let backend = await AppKitBackend ( )
148+ let cancellable = state. didChange. observeAsUIUpdater ( backend: backend) {
149+ Task {
150+ await updateCount. update { $0 + 1 }
151+ }
152+ // Simulate an update of duration `updateDuration` seconds
153+ Thread . sleep ( forTimeInterval: updateDuration)
154+ }
155+ _ = cancellable // Silence warning about cancellable being unused
156+
157+ let start = ProcessInfo . processInfo. systemUptime
158+ for _ in 0 ..< mutationCount {
159+ state. count += 1
160+ try ? await Task . sleep ( for: . seconds( mutationGap) )
161+ }
162+ let elapsed = ProcessInfo . processInfo. systemUptime - start
163+
164+ // Compute percentage of main thread's time taken up by updates.
165+ let ratio = Double ( await updateCount. count) * updateDuration / elapsed
166+ XCTAssert (
167+ 0.5 <= ratio && ratio <= 0.85 ,
168+ """
169+ Expected throttled updates to take between 50% and 80% of the main \
170+ thread's time. Took \( Int ( ratio * 100 ) ) %
171+ """
172+ )
173+ }
174+
68175 @MainActor
69176 func testBasicLayout( ) async throws {
70177 let backend = AppKitBackend ( )
0 commit comments