diff --git a/Configurations/OpenSwiftUIUITests.xcconfig b/Configurations/OpenSwiftUIUITests.xcconfig index 1d072a0a7..e304bd0f1 100644 --- a/Configurations/OpenSwiftUIUITests.xcconfig +++ b/Configurations/OpenSwiftUIUITests.xcconfig @@ -4,3 +4,7 @@ CODE_SIGN_ENTITLEMENTS = TestingHost/TestingHost.entitlements // Remove -module_alias Testing=_Testing_Unavailable OTHER_SWIFT_FLAGS = $(OPENSWIFTUI_AVAILABILITY_MACRO) + +CLANG_ENABLE_MODULES = YES; + +SWIFT_OBJC_BRIDGING_HEADER = OpenSwiftUIUITests/OpenSwiftUIUITests-Bridging-Header.h diff --git a/Example/OpenSwiftUIUITests/Animation/Animation/AnimationCompletionUITests.swift b/Example/OpenSwiftUIUITests/Animation/Animation/AnimationCompletionUITests.swift new file mode 100644 index 000000000..be51e8ef5 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Animation/Animation/AnimationCompletionUITests.swift @@ -0,0 +1,56 @@ +// +// AnimationCompletionUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct AnimationCompletionUITests { + @Test + func logicalAndRemovedComplete() { + struct ContentView: AnimationTestView { + nonisolated static var model: AnimationTestModel { + AnimationTestModel(duration: 3, count: 3) + } + + @State private var opacity = 1.0 + @State private var scale = 1.0 + + @State private var showGreen = false + @State private var showBlue = false + + var body: some View { + ZStack { + Color.red + .frame(width: 100 * scale, height: 100 * scale) + .opacity(opacity) + if showGreen { Color.green.frame(width: 20, height: 20) } + if showBlue { Color.blue.frame(width: 10, height: 10) } + } + .onAppear { + let animation = Animation.linear(duration: 2) + .logicallyComplete(after: 1) + withAnimation(animation, completionCriteria: .logicallyComplete) { + opacity = 0.1 + } completion: { + showGreen.toggle() + } + withAnimation(animation, completionCriteria: .removed) { + scale = 0.1 + } completion: { + showBlue.toggle() + } + } + } + } + // When run seperately, precision: 1.0 works fine + // but in the full suite, it will hit the same issue of #340 + withKnownIssue("#340", isIntermittent: true) { + openSwiftUIAssertAnimationSnapshot( + of: ContentView() + ) + } + } +} diff --git a/Example/OpenSwiftUIUITests/Animation/Animation/BezierAnimationUITests.swift b/Example/OpenSwiftUIUITests/Animation/Animation/BezierAnimationUITests.swift new file mode 100644 index 000000000..8d60a0e4f --- /dev/null +++ b/Example/OpenSwiftUIUITests/Animation/Animation/BezierAnimationUITests.swift @@ -0,0 +1,86 @@ +// +// BezierAnimationUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct BezierAnimationUITests { + @Test + func colorRow() { + struct ContentView: AnimationTestView { + nonisolated static var model: AnimationTestModel { + AnimationTestModel(duration: 2, count: 5) + } + + @State private var animate = false + private var animationDuration: Double { Self.model.duration } + + var body: some View { + VStack(spacing: 10) { + AnimationRow( + color: .blue, + animate: animate, + animation: .linear(duration: animationDuration) + ) + AnimationRow( + color: .green, + animate: animate, + animation: .easeIn(duration: animationDuration) + ) + AnimationRow( + color: .red, + animate: animate, + animation: .easeOut(duration: animationDuration) + ) + AnimationRow( + color: .orange, + animate: animate, + animation: .easeInOut(duration: animationDuration) + ) + AnimationRow( + color: .purple, + animate: animate, + animation: .timingCurve(0.68, -0.6, 0.32, 1.6, duration: animationDuration) + ) + } + .padding() + .frame(width: 200, height: 200) + .onAppear { + animate.toggle() + } + } + struct AnimationRow: View { + let color: Color + let animate: Bool + let animation: Animation + + private let squareSize: CGFloat = 20.0 + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .leading) { + Color.black.opacity(0.1) + color + // TODO: Color interpolation not aligned yet + // Color(animate ? color : .gray) + .frame(width: squareSize, height: squareSize) + .offset(x: animate ? geometry.size.width - squareSize : 0) + } + } + .frame(height: squareSize) + .animation(animation, value: animate) + } + } + } + // When run seperately, precision: 1.0 works fine + // but in the full suite, it will hit the same issue of #340 + withKnownIssue("$340", isIntermittent: true) { + openSwiftUIAssertAnimationSnapshot( + of: ContentView(), + ) + } + } +} diff --git a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift index c2acbcd04..3b30bfc44 100644 --- a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift +++ b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift @@ -75,7 +75,9 @@ struct ColorUITests { } } } - openSwiftUIAssertAnimationSnapshot(of: ContentView()) + withKnownIssue("#340", isIntermittent: true) { + openSwiftUIAssertAnimationSnapshot(of: ContentView()) + } } // FIXME diff --git a/Example/OpenSwiftUIUITests/Layout/Dynamic/DynamicLayoutViewUITests.swift b/Example/OpenSwiftUIUITests/Layout/Dynamic/DynamicLayoutViewUITests.swift index f66cea698..d820cb2bb 100644 --- a/Example/OpenSwiftUIUITests/Layout/Dynamic/DynamicLayoutViewUITests.swift +++ b/Example/OpenSwiftUIUITests/Layout/Dynamic/DynamicLayoutViewUITests.swift @@ -11,7 +11,7 @@ struct DynamicLayoutViewUITests { @Test func dynamicLayout() { struct ContentView: View { - @State var show = false + @State private var show = false var body: some View { VStack { Color.red diff --git a/Example/OpenSwiftUIUITests/OpenSwiftUIUITests-Bridging-Header.h b/Example/OpenSwiftUIUITests/OpenSwiftUIUITests-Bridging-Header.h new file mode 100644 index 000000000..dfb27df13 --- /dev/null +++ b/Example/OpenSwiftUIUITests/OpenSwiftUIUITests-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "AnimationDebugControllerHelper.h" diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift index f4c7e1263..7d76993f7 100644 --- a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift @@ -3,7 +3,6 @@ // OpenSwiftUIUITests import Foundation -import OpenSwiftUI_SPI struct AnimationTestModel: Hashable { var intervals: [Double] @@ -99,17 +98,23 @@ final class AnimationDebugController: PlatformHostingController where V: V // Avoid generic parameter casting private protocol AnimationDebuggableController: PlatformViewController { var disableLayoutSubview: Bool { get set } + + func advance(interval: Double) } extension AnimationDebugController: AnimationDebuggableController {} extension PlatformView { + // Fix swift-snapshot-testing trigger extra layoutSubviews and advance time issue @objc func swizzled_layoutSubviews() { guard let vc = _viewControllerForAncestor as? AnimationDebuggableController else { swizzled_layoutSubviews() return } guard !vc.disableLayoutSubview else { + // superLayoutSubviews(type(of: self)) + // Fix swift-snapshot-testing set initialFrame as .zero and then trigger setProposedSize(.zero) causing DisplayList layout issue (View is placed at topLeft instead of center) + vc.advance(interval: .zero) return } swizzled_layoutSubviews() diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugControllerHelper.h b/Example/OpenSwiftUIUITests/UITests/AnimationDebugControllerHelper.h new file mode 100644 index 000000000..ea70946d7 --- /dev/null +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugControllerHelper.h @@ -0,0 +1,22 @@ +// +// AnimationDebugControllerHelper.h +// OpenSwiftUIUITests + +#import + +#if TARGET_OS_IPHONE + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIView (OpenSwiftUIUITests) +@property(nonatomic, readonly, nullable) UIViewController *_viewControllerForAncestor; +@end + +void superLayoutSubviews(Class cls); + +NS_ASSUME_NONNULL_END + + +#endif diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugControllerHelper.m b/Example/OpenSwiftUIUITests/UITests/AnimationDebugControllerHelper.m new file mode 100644 index 000000000..56c682538 --- /dev/null +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugControllerHelper.m @@ -0,0 +1,15 @@ +// +// AnimationDebugControllerHelper.m +// OpenSwiftUIUITests + +#import "AnimationDebugControllerHelper.h" +#import +#import + +#if TARGET_OS_IPHONE + +void superLayoutSubviews(Class cls) { + ((void (*)(struct objc_super *, SEL))(void *)objc_msgSendSuper)(&((struct objc_super){(id)cls, (id)class_getSuperclass(cls)}), sel_registerName("layoutSubviews")); +} + +#endif diff --git a/Example/OpenSwiftUIUITests/View/DynamicViewContent/ForEachUITests.swift b/Example/OpenSwiftUIUITests/View/DynamicViewContent/ForEachUITests.swift index f3cf7604b..6f018ea8e 100644 --- a/Example/OpenSwiftUIUITests/View/DynamicViewContent/ForEachUITests.swift +++ b/Example/OpenSwiftUIUITests/View/DynamicViewContent/ForEachUITests.swift @@ -37,4 +37,32 @@ struct ForEachUITests { } openSwiftUIAssertSnapshot(of: ContentView()) } + + @Test + func insertAnimation() { + struct ContentView: AnimationTestView { + nonisolated static var model: AnimationTestModel { + AnimationTestModel(duration: 1.0, count: 5) + } + + @State private var opacities = [0, 0.5, 1.0] + + var body: some View { + VStack(spacing: 0) { + ForEach(opacities, id: \.self) { opacity in + Color.red.opacity(opacity) + } + }.onAppear { + withAnimation(.spring(duration: Self.model.duration)) { + opacities.insert(0.25, at: 1) + opacities.insert(0.75, at: 3) + } + } + } + } + withKnownIssue("#632") { + Issue.record("AttributeGraph precondition failure: accessing attribute in a different namespace: 36376.") + // openSwiftUIAssertAnimationSnapshot(of: ContentView()) + } + } } diff --git a/Example/SharedExample/Layout/Dynamic/DynamicLayoutViewExample.swift b/Example/SharedExample/Layout/Dynamic/DynamicLayoutViewExample.swift index 2a498c7c8..4491f2fe4 100644 --- a/Example/SharedExample/Layout/Dynamic/DynamicLayoutViewExample.swift +++ b/Example/SharedExample/Layout/Dynamic/DynamicLayoutViewExample.swift @@ -9,7 +9,7 @@ import SwiftUI #endif struct DynamicLayoutViewExample: View { - @State var show = false + @State private var show = false var body: some View { VStack { Color.red diff --git a/Example/SharedExample/View/DynamicContentView/ForEachExample.swift b/Example/SharedExample/View/DynamicContentView/ForEachExample.swift index c6ba9f7d6..d50e3a73d 100644 --- a/Example/SharedExample/View/DynamicContentView/ForEachExample.swift +++ b/Example/SharedExample/View/DynamicContentView/ForEachExample.swift @@ -42,9 +42,8 @@ struct ForEachKeyPathExample: View { } } -// TODO: Animation test case struct ForEachDynamicView: View { - @State var opacities = [0, 0.5, 1.0] + @State private var opacities = [0, 0.5, 1.0] var body: some View { VStack(spacing: 0) { diff --git a/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompletionCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompletionCompatibilityTests.swift index 1183b49ca..0f45f48e0 100644 --- a/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompletionCompatibilityTests.swift +++ b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompletionCompatibilityTests.swift @@ -56,6 +56,8 @@ struct AnimationCompletionCompatibilityTests { ) ) } - #expect(Helper.values == [1, 2]) + withKnownIssue(isIntermittent: true) { + #expect(Helper.values == [1, 2]) + } } } diff --git a/Tests/OpenSwiftUICompatibilityTests/View/Debug/ChangedBodyPropertyCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/View/Debug/ChangedBodyPropertyCompatibilityTests.swift index dc9fff1d3..c4f93d268 100644 --- a/Tests/OpenSwiftUICompatibilityTests/View/Debug/ChangedBodyPropertyCompatibilityTests.swift +++ b/Tests/OpenSwiftUICompatibilityTests/View/Debug/ChangedBodyPropertyCompatibilityTests.swift @@ -64,7 +64,7 @@ struct ChangedBodyPropertyCompatibilityTests { @Test func statePropertyView() throws { struct ContentView: View { - @State var name = "" + @State private var name = "" var body: some View { let _ = Self._logChanges() AnyView(EmptyView())