@@ -64,7 +64,114 @@ final class SwiftCrossUITests: XCTestCase {
64
64
return original == decoded
65
65
}
66
66
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
+
67
119
#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
+
68
175
@MainActor
69
176
func testBasicLayout( ) async throws {
70
177
let backend = AppKitBackend ( )
0 commit comments