Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@ jobs:
name: macOS
strategy:
matrix:
xcode: ['16.4']
xcode: ['16.4', '26.0']
config:
- debug
# - release
- release
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: List available devices
run: xcrun simctl list devices available
# NB: GitHub CI only contains newer runtimes
# - name: Run ${{ matrix.config }} tests
# run: swift test -c ${{ matrix.config }}
if: ${{ matrix.config == 'debug' }}
# NB: GitHub CI only contains newer runtimes, but we force tests to build against deployment target predating Observation
- name: Run ${{ matrix.config }} tests
run: swift test -c ${{ matrix.config }} -Xswiftc -target -Xswiftc arm64-apple-macosx13.0
- name: Run compatibility tests
run: make test-compatibility
if: ${{ matrix.config == 'debug' }}
Expand All @@ -45,15 +46,15 @@ jobs:
runs-on: ubuntu-latest
container: swift:${{ matrix.swift }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build
run: swift build

wasm:
name: Wasm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: bytecodealliance/actions/wasmtime/setup@v1
- name: Install Swift and Swift SDK for WebAssembly
run: |
Expand Down
190 changes: 106 additions & 84 deletions Tests/PerceptionTests/PerceptionCheckingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,25 @@
import SwiftUI
import XCTest

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@available(iOS, introduced: 16, deprecated: 17, obsoleted: 17)
@available(macOS, introduced: 13, deprecated: 14, obsoleted: 14)
@available(tvOS, introduced: 16, deprecated: 17, obsoleted: 17)
@available(watchOS, introduced: 9, deprecated: 10, obsoleted: 10)
final class PerceptionCheckingTests: XCTestCase {
override func setUp() async throws {
guard !deploymentTargetIncludesObservation() else {
throw XCTSkip(
"""
PercecptionTests were built against a deployment target too recent for perception checking.

To force these tests to run on macOS, you can override the target OS version explicitly as:

swift test -Xswiftc -target -Xswiftc arm64-apple-macosx13.0
"""
)
}
}

@MainActor
func testNotInPerceptionBody() {
let model = Model()
Expand Down Expand Up @@ -65,35 +82,35 @@
try await render(FeatureView())
}

#if !os(macOS)
@MainActor
func testNotInPerceptionBody_SwiftUIBinding() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
Form {
TextField("", text: expectRuntimeWarning { $model.text })
}
@MainActor
func testNotInPerceptionBody_SwiftUIBinding() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
Form {
TextField("", text: expectRuntimeWarning { $model.text })
}
}
try await render(FeatureView(model: Model()))
}
#endif
#if os(macOS)
// NB: This failure is triggered out-of-body by the binding.
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
#endif
try await render(FeatureView(model: Model()))
}

#if !os(macOS)
@MainActor
func testInPerceptionBody_SwiftUIBinding() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
WithPerceptionTracking {
TextField("", text: $model.text)
}
@MainActor
func testInPerceptionBody_SwiftUIBinding() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
WithPerceptionTracking {
TextField("", text: $model.text)
}
}
try await render(FeatureView(model: Model()))
}
#endif
try await render(FeatureView(model: Model()))
}

@MainActor
func testNotInPerceptionBody_ForEach() async throws {
Expand Down Expand Up @@ -199,83 +216,75 @@
)
}

#if !os(macOS)
@MainActor
func testNotInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
@MainActor
func testNotInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
Text("Parent")
.sheet(item: expectRuntimeWarning { $model.child }) { child in
Text(expectRuntimeWarning { child.count }.description)
}
}
}
// NB: This failure is triggered out-of-body by the binding.
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
try await render(FeatureView(model: Model(child: Model())))
}

@MainActor
func testInnerInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
Text("Parent")
.sheet(item: expectRuntimeWarning { $model.child }) { child in
WithPerceptionTracking {
Text(child.count.description)
}
}
}
}
// NB: This failure is triggered out-of-body by the binding.
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
try await render(FeatureView(model: Model(child: Model())))
}

@MainActor
func testOuterInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
WithPerceptionTracking {
Text("Parent")
.sheet(item: expectRuntimeWarning { $model.child }) { child in
.sheet(item: $model.child) { child in
Text(expectRuntimeWarning { child.count }.description)
}
}
}
// NB: This failure is triggered out-of-body by the binding.
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
try await render(FeatureView(model: Model(child: Model())))
}
#endif

#if !os(macOS)
@MainActor
func testInnerInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
try await render(FeatureView(model: Model(child: Model())))
}

@MainActor
func testOuterAndInnerInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
WithPerceptionTracking {
Text("Parent")
.sheet(item: expectRuntimeWarning { $model.child }) { child in
.sheet(item: $model.child) { child in
WithPerceptionTracking {
Text(child.count.description)
}
}
}
}
// NB: This failure is triggered out-of-body by the binding.
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
try await render(FeatureView(model: Model(child: Model())))
}
#endif

#if !os(macOS)
@MainActor
func testOuterInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
WithPerceptionTracking {
Text("Parent")
.sheet(item: $model.child) { child in
Text(expectRuntimeWarning { child.count }.description)
}
}
}
}

try await render(FeatureView(model: Model(child: Model())))
}
#endif

#if !os(macOS)
@MainActor
func testOuterAndInnerInPerceptionBody_Sheet() async throws {
struct FeatureView: View {
@Perception.Bindable var model: Model
var body: some View {
WithPerceptionTracking {
Text("Parent")
.sheet(item: $model.child) { child in
WithPerceptionTracking {
Text(child.count.description)
}
}
}
}
}

try await render(FeatureView(model: Model(child: Model())))
}
#endif
try await render(FeatureView(model: Model(child: Model())))
}

@MainActor
func testActionClosure() async throws {
Expand Down Expand Up @@ -597,6 +606,7 @@

@MainActor
private func render(_ view: some View) async throws {
try checkImageRendererAvailable()
let image = ImageRenderer(content: view).cgImage
_ = image
try await Task.sleep(for: .seconds(0.1))
Expand Down Expand Up @@ -635,4 +645,16 @@
self.content
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
private func deploymentTargetIncludesObservation() -> Bool { true }

@_disfavoredOverload
private func deploymentTargetIncludesObservation(_dummy: Void = ()) -> Bool { false }

private func checkImageRendererAvailable() throws {
guard #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) else {
throw XCTSkip("This test requires 'SwiftUI.ImageRenderer' to be available.")
}
}
#endif
4 changes: 4 additions & 0 deletions Tests/PerceptionTests/PerceptionTrackingTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Perception
import XCTest

@available(iOS, introduced: 16, deprecated: 17)
@available(macOS, introduced: 13, deprecated: 14)
@available(tvOS, introduced: 16, deprecated: 17)
@available(watchOS, introduced: 9, deprecated: 10)
final class PerceptionTrackingTests: XCTestCase {
var isComplete = false

Expand Down