|
4 | 4 | import SwiftUI |
5 | 5 | import XCTest |
6 | 6 |
|
7 | | - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) |
| 7 | + @available(iOS, introduced: 16, deprecated: 17, obsoleted: 17) |
| 8 | + @available(macOS, introduced: 13, deprecated: 14, obsoleted: 14) |
| 9 | + @available(tvOS, introduced: 16, deprecated: 17, obsoleted: 17) |
| 10 | + @available(watchOS, introduced: 9, deprecated: 10, obsoleted: 10) |
8 | 11 | final class PerceptionCheckingTests: XCTestCase { |
| 12 | + override func setUp() async throws { |
| 13 | + guard !deploymentTargetIncludesObservation() else { |
| 14 | + throw XCTSkip( |
| 15 | + """ |
| 16 | + PerceptionTests were built against a deployment target too recent for perception checking. |
| 17 | +
|
| 18 | + To force these tests to run on macOS, you can override the target OS version explicitly as: |
| 19 | +
|
| 20 | + swift test -Xswiftc -target -Xswiftc arm64-apple-macosx13.0 |
| 21 | + """ |
| 22 | + ) |
| 23 | + } |
| 24 | + } |
| 25 | + |
9 | 26 | @MainActor |
10 | 27 | func testNotInPerceptionBody() { |
11 | 28 | let model = Model() |
|
65 | 82 | try await render(FeatureView()) |
66 | 83 | } |
67 | 84 |
|
68 | | - #if !os(macOS) |
69 | | - @MainActor |
70 | | - func testNotInPerceptionBody_SwiftUIBinding() async throws { |
71 | | - struct FeatureView: View { |
72 | | - @Perception.Bindable var model: Model |
73 | | - var body: some View { |
74 | | - Form { |
75 | | - TextField("", text: expectRuntimeWarning { $model.text }) |
76 | | - } |
| 85 | + @MainActor |
| 86 | + func testNotInPerceptionBody_SwiftUIBinding() async throws { |
| 87 | + struct FeatureView: View { |
| 88 | + @Perception.Bindable var model: Model |
| 89 | + var body: some View { |
| 90 | + Form { |
| 91 | + TextField("", text: expectRuntimeWarning { $model.text }) |
77 | 92 | } |
78 | 93 | } |
79 | | - try await render(FeatureView(model: Model())) |
80 | 94 | } |
81 | | - #endif |
| 95 | + #if os(macOS) |
| 96 | + // NB: This failure is triggered out-of-body by the binding. |
| 97 | + XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") } |
| 98 | + #endif |
| 99 | + try await render(FeatureView(model: Model())) |
| 100 | + } |
82 | 101 |
|
83 | | - #if !os(macOS) |
84 | | - @MainActor |
85 | | - func testInPerceptionBody_SwiftUIBinding() async throws { |
86 | | - struct FeatureView: View { |
87 | | - @Perception.Bindable var model: Model |
88 | | - var body: some View { |
89 | | - WithPerceptionTracking { |
90 | | - TextField("", text: $model.text) |
91 | | - } |
| 102 | + @MainActor |
| 103 | + func testInPerceptionBody_SwiftUIBinding() async throws { |
| 104 | + struct FeatureView: View { |
| 105 | + @Perception.Bindable var model: Model |
| 106 | + var body: some View { |
| 107 | + WithPerceptionTracking { |
| 108 | + TextField("", text: $model.text) |
92 | 109 | } |
93 | 110 | } |
94 | | - try await render(FeatureView(model: Model())) |
95 | 111 | } |
96 | | - #endif |
| 112 | + try await render(FeatureView(model: Model())) |
| 113 | + } |
97 | 114 |
|
98 | 115 | @MainActor |
99 | 116 | func testNotInPerceptionBody_ForEach() async throws { |
|
199 | 216 | ) |
200 | 217 | } |
201 | 218 |
|
202 | | - #if !os(macOS) |
203 | | - @MainActor |
204 | | - func testNotInPerceptionBody_Sheet() async throws { |
205 | | - struct FeatureView: View { |
206 | | - @Perception.Bindable var model: Model |
207 | | - var body: some View { |
| 219 | + @MainActor |
| 220 | + func testNotInPerceptionBody_Sheet() async throws { |
| 221 | + struct FeatureView: View { |
| 222 | + @Perception.Bindable var model: Model |
| 223 | + var body: some View { |
| 224 | + Text("Parent") |
| 225 | + .sheet(item: expectRuntimeWarning { $model.child }) { child in |
| 226 | + Text(expectRuntimeWarning { child.count }.description) |
| 227 | + } |
| 228 | + } |
| 229 | + } |
| 230 | + // NB: This failure is triggered out-of-body by the binding. |
| 231 | + XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") } |
| 232 | + try await render(FeatureView(model: Model(child: Model()))) |
| 233 | + } |
| 234 | + |
| 235 | + @MainActor |
| 236 | + func testInnerInPerceptionBody_Sheet() async throws { |
| 237 | + struct FeatureView: View { |
| 238 | + @Perception.Bindable var model: Model |
| 239 | + var body: some View { |
| 240 | + Text("Parent") |
| 241 | + .sheet(item: expectRuntimeWarning { $model.child }) { child in |
| 242 | + WithPerceptionTracking { |
| 243 | + Text(child.count.description) |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + } |
| 248 | + // NB: This failure is triggered out-of-body by the binding. |
| 249 | + XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") } |
| 250 | + try await render(FeatureView(model: Model(child: Model()))) |
| 251 | + } |
| 252 | + |
| 253 | + @MainActor |
| 254 | + func testOuterInPerceptionBody_Sheet() async throws { |
| 255 | + struct FeatureView: View { |
| 256 | + @Perception.Bindable var model: Model |
| 257 | + var body: some View { |
| 258 | + WithPerceptionTracking { |
208 | 259 | Text("Parent") |
209 | | - .sheet(item: expectRuntimeWarning { $model.child }) { child in |
| 260 | + .sheet(item: $model.child) { child in |
210 | 261 | Text(expectRuntimeWarning { child.count }.description) |
211 | 262 | } |
212 | 263 | } |
213 | 264 | } |
214 | | - // NB: This failure is triggered out-of-body by the binding. |
215 | | - XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") } |
216 | | - try await render(FeatureView(model: Model(child: Model()))) |
217 | 265 | } |
218 | | - #endif |
219 | 266 |
|
220 | | - #if !os(macOS) |
221 | | - @MainActor |
222 | | - func testInnerInPerceptionBody_Sheet() async throws { |
223 | | - struct FeatureView: View { |
224 | | - @Perception.Bindable var model: Model |
225 | | - var body: some View { |
| 267 | + try await render(FeatureView(model: Model(child: Model()))) |
| 268 | + } |
| 269 | + |
| 270 | + @MainActor |
| 271 | + func testOuterAndInnerInPerceptionBody_Sheet() async throws { |
| 272 | + struct FeatureView: View { |
| 273 | + @Perception.Bindable var model: Model |
| 274 | + var body: some View { |
| 275 | + WithPerceptionTracking { |
226 | 276 | Text("Parent") |
227 | | - .sheet(item: expectRuntimeWarning { $model.child }) { child in |
| 277 | + .sheet(item: $model.child) { child in |
228 | 278 | WithPerceptionTracking { |
229 | 279 | Text(child.count.description) |
230 | 280 | } |
231 | 281 | } |
232 | 282 | } |
233 | 283 | } |
234 | | - // NB: This failure is triggered out-of-body by the binding. |
235 | | - XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") } |
236 | | - try await render(FeatureView(model: Model(child: Model()))) |
237 | | - } |
238 | | - #endif |
239 | | - |
240 | | - #if !os(macOS) |
241 | | - @MainActor |
242 | | - func testOuterInPerceptionBody_Sheet() async throws { |
243 | | - struct FeatureView: View { |
244 | | - @Perception.Bindable var model: Model |
245 | | - var body: some View { |
246 | | - WithPerceptionTracking { |
247 | | - Text("Parent") |
248 | | - .sheet(item: $model.child) { child in |
249 | | - Text(expectRuntimeWarning { child.count }.description) |
250 | | - } |
251 | | - } |
252 | | - } |
253 | | - } |
254 | | - |
255 | | - try await render(FeatureView(model: Model(child: Model()))) |
256 | 284 | } |
257 | | - #endif |
258 | | - |
259 | | - #if !os(macOS) |
260 | | - @MainActor |
261 | | - func testOuterAndInnerInPerceptionBody_Sheet() async throws { |
262 | | - struct FeatureView: View { |
263 | | - @Perception.Bindable var model: Model |
264 | | - var body: some View { |
265 | | - WithPerceptionTracking { |
266 | | - Text("Parent") |
267 | | - .sheet(item: $model.child) { child in |
268 | | - WithPerceptionTracking { |
269 | | - Text(child.count.description) |
270 | | - } |
271 | | - } |
272 | | - } |
273 | | - } |
274 | | - } |
275 | 285 |
|
276 | | - try await render(FeatureView(model: Model(child: Model()))) |
277 | | - } |
278 | | - #endif |
| 286 | + try await render(FeatureView(model: Model(child: Model()))) |
| 287 | + } |
279 | 288 |
|
280 | 289 | @MainActor |
281 | 290 | func testActionClosure() async throws { |
|
597 | 606 |
|
598 | 607 | @MainActor |
599 | 608 | private func render(_ view: some View) async throws { |
| 609 | + try checkImageRendererAvailable() |
600 | 610 | let image = ImageRenderer(content: view).cgImage |
601 | 611 | _ = image |
602 | 612 | try await Task.sleep(for: .seconds(0.1)) |
|
635 | 645 | self.content |
636 | 646 | } |
637 | 647 | } |
| 648 | + |
| 649 | + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
| 650 | + private func deploymentTargetIncludesObservation() -> Bool { true } |
| 651 | + |
| 652 | + @_disfavoredOverload |
| 653 | + private func deploymentTargetIncludesObservation(_: Void = ()) -> Bool { false } |
| 654 | + |
| 655 | + private func checkImageRendererAvailable() throws { |
| 656 | + guard #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) else { |
| 657 | + throw XCTSkip("This test requires 'SwiftUI.ImageRenderer' to be available.") |
| 658 | + } |
| 659 | + } |
638 | 660 | #endif |
0 commit comments