diff --git a/Hero.xcodeproj/project.pbxproj b/Hero.xcodeproj/project.pbxproj index 1cab81a3..a7ba9d09 100644 --- a/Hero.xcodeproj/project.pbxproj +++ b/Hero.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ B35264CF2454FEF300D33861 /* Locale+Hero.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35264CD2454FEF300D33861 /* Locale+Hero.swift */; }; B383074925D1041A00B7A0D8 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B383074825D1041A00B7A0D8 /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B383074B25D1042C00B7A0D8 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B383074A25D1042C00B7A0D8 /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + D347BDBC2ED41295009608AE /* HeroModifierInputValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D347BDBB2ED41295009608AE /* HeroModifierInputValidationTests.swift */; }; DBA05BB41A704A4A17967918 /* Pods_HeroTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841FFA357ACB279D3F74CDEE /* Pods_HeroTests.framework */; }; F482F0BE235D7808002E97ED /* UIColor+HexString.swift in Sources */ = {isa = PBXBuildFile; fileRef = F482F0BD235D7808002E97ED /* UIColor+HexString.swift */; }; F482F0BF235D7808002E97ED /* UIColor+HexString.swift in Sources */ = {isa = PBXBuildFile; fileRef = F482F0BD235D7808002E97ED /* UIColor+HexString.swift */; }; @@ -294,6 +295,7 @@ B383074A25D1042C00B7A0D8 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS14.3.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; C377744CBFF1E24426E80F55 /* Pods-HeroExamples.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HeroExamples.release.xcconfig"; path = "Pods/Target Support Files/Pods-HeroExamples/Pods-HeroExamples.release.xcconfig"; sourceTree = ""; }; C51A6465EC2CB38D82F28B93 /* Pods-HeroTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HeroTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-HeroTests/Pods-HeroTests.debug.xcconfig"; sourceTree = ""; }; + D347BDBB2ED41295009608AE /* HeroModifierInputValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroModifierInputValidationTests.swift; sourceTree = ""; }; EEE340F89FF0A49DD23A5A6E /* Pods_HeroExamples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HeroExamples.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F482F0BD235D7808002E97ED /* UIColor+HexString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+HexString.swift"; sourceTree = ""; }; F482F0C5235D7C4C002E97ED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; @@ -590,6 +592,7 @@ AF1E1B521E66822C00ECE039 /* Tests */ = { isa = PBXGroup; children = ( + D347BDBB2ED41295009608AE /* HeroModifierInputValidationTests.swift */, AF1E1B531E66822C00ECE039 /* HeroTests.swift */, AF1E1B551E66822C00ECE039 /* Info.plist */, ); @@ -896,10 +899,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HeroExamples/Pods-HeroExamples-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-HeroExamples/Pods-HeroExamples-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HeroExamples/Pods-HeroExamples-frameworks.sh\"\n"; @@ -1126,6 +1133,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D347BDBC2ED41295009608AE /* HeroModifierInputValidationTests.swift in Sources */, AF1E1B541E66822C00ECE039 /* HeroTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1294,9 +1302,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; @@ -1305,7 +1311,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1327,7 +1333,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; @@ -1336,7 +1342,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1402,7 +1408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.6.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -1458,7 +1464,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.6.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; @@ -1477,7 +1483,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Examples/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1509,7 +1515,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Examples/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1542,7 +1548,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1576,7 +1582,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Podfile b/Podfile index 719157e8..01d1e34b 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ target 'HeroExamples' do - platform :ios, '10.0' + platform :ios, '12.0' use_frameworks! pod 'CollectionKit', :inhibit_warnings => true @@ -11,6 +11,19 @@ target 'HeroExamples' do end target 'HeroTvOSExamples' do - platform :tvos, '10.0' + platform :tvos, '12.0' use_frameworks! end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 12.0 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + end + if config.build_settings['TVOS_DEPLOYMENT_TARGET'].to_f < 12.0 + config.build_settings['TVOS_DEPLOYMENT_TARGET'] = '12.0' + end + end + end +end diff --git a/Podfile.lock b/Podfile.lock index 389011cc..5bf4b437 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -13,6 +13,6 @@ SPEC REPOS: SPEC CHECKSUMS: CollectionKit: 8f01e7629185bb81072c4aa734d105df5c2d1c8b -PODFILE CHECKSUM: 5c14933c915eeee6fbe5ecdd950d3da01c4a0a86 +PODFILE CHECKSUM: 4b47eb3c33ba27c86d91d0651803ab1d0a102e64 -COCOAPODS: 1.11.3 +COCOAPODS: 1.16.2 diff --git a/Sources/HeroModifier.swift b/Sources/HeroModifier.swift index 286c36d9..2dd6a1c1 100644 --- a/Sources/HeroModifier.swift +++ b/Sources/HeroModifier.swift @@ -54,6 +54,10 @@ extension HeroModifier { - position: position for the view to animate from/to */ public static func position(_ position: CGPoint) -> HeroModifier { + guard position.x.isFinite, position.y.isFinite else { + assertionFailure("Hero: Invalid position values (NaN or Infinity). Position: \(position)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.position = position } @@ -65,6 +69,10 @@ extension HeroModifier { - size: size for the view to animate from/to */ public static func size(_ size: CGSize) -> HeroModifier { + guard size.width.isFinite, size.height.isFinite else { + assertionFailure("Hero: Invalid size values (NaN or Infinity). Size: \(size)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.size = size } @@ -90,6 +98,10 @@ extension HeroModifier { - perspective: set the camera distance of the transform */ public static func perspective(_ perspective: CGFloat) -> HeroModifier { + guard perspective.isFinite, perspective != 0 else { + assertionFailure("Hero: Invalid perspective value (NaN, Infinity, or zero). Perspective: \(perspective)") + return HeroModifier { _ in } + } return HeroModifier { targetState in var transform = targetState.transform ?? CATransform3DIdentity transform.m34 = 1.0 / -perspective @@ -105,6 +117,10 @@ extension HeroModifier { - z: scale factor on z axis, default 1 */ public static func scale(x: CGFloat = 1, y: CGFloat = 1, z: CGFloat = 1) -> HeroModifier { + guard x.isFinite, y.isFinite, z.isFinite else { + assertionFailure("Hero: Invalid scale values (NaN or Infinity). x: \(x), y: \(y), z: \(z)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.transform = CATransform3DScale(targetState.transform ?? CATransform3DIdentity, x, y, z) } @@ -127,6 +143,10 @@ extension HeroModifier { - z: translation distance on z axis in display pixel, default 0 */ public static func translate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> HeroModifier { + guard x.isFinite, y.isFinite, z.isFinite else { + assertionFailure("Hero: Invalid translate values (NaN or Infinity). x: \(x), y: \(y), z: \(z)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.transform = CATransform3DTranslate(targetState.transform ?? CATransform3DIdentity, x, y, z) } @@ -144,6 +164,10 @@ extension HeroModifier { - z: rotation on z axis in radian, default 0 */ public static func rotate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> HeroModifier { + guard x.isFinite, y.isFinite, z.isFinite else { + assertionFailure("Hero: Invalid rotate values (NaN or Infinity). x: \(x), y: \(y), z: \(z)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.transform = CATransform3DRotate(targetState.transform ?? CATransform3DIdentity, x, 1, 0, 0) targetState.transform = CATransform3DRotate(targetState.transform!, y, 0, 1, 0) @@ -342,10 +366,18 @@ extension HeroModifier { Sets the duration of the animation for a given view. If not used, Hero will use determine the duration based on the distance and size changes. - Parameters: - duration: duration of the animation - + Note: a duration of .infinity means matching the duration of the longest animation. same as .durationMatchLongest */ public static func duration(_ duration: TimeInterval) -> HeroModifier { + guard duration.isFinite || duration == .infinity else { + assertionFailure("Hero: Invalid duration value (NaN). Duration: \(duration)") + return HeroModifier { _ in } + } + guard duration >= 0 else { + assertionFailure("Hero: Duration must be non-negative. Duration: \(duration)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.duration = duration } @@ -364,6 +396,14 @@ extension HeroModifier { - delay: delay of the animation */ public static func delay(_ delay: TimeInterval) -> HeroModifier { + guard delay.isFinite else { + assertionFailure("Hero: Invalid delay value (NaN or Infinity). Delay: \(delay)") + return HeroModifier { _ in } + } + guard delay >= 0 else { + assertionFailure("Hero: Delay must be non-negative. Delay: \(delay)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.delay = delay } @@ -439,6 +479,10 @@ extension HeroModifier { default is 1. */ public static func arc(intensity: CGFloat = 1) -> HeroModifier { + guard intensity.isFinite else { + assertionFailure("Hero: Invalid arc intensity value (NaN or Infinity). Intensity: \(intensity)") + return HeroModifier { _ in } + } return HeroModifier { targetState in targetState.arc = intensity } diff --git a/Tests/HeroModifierInputValidationTests.swift b/Tests/HeroModifierInputValidationTests.swift new file mode 100644 index 00000000..6df1b7ae --- /dev/null +++ b/Tests/HeroModifierInputValidationTests.swift @@ -0,0 +1,206 @@ +// The MIT License (MIT) +// +// Copyright (c) 2016 Luke Zhao +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(UIKit) +import XCTest +import Hero + +class HeroModifierInputValidationTests: XCTestCase { + + // MARK: - Position Modifier Tests + + func testPositionWithValidValues() { + let modifier = HeroModifier.position(CGPoint(x: 100, y: 200)) + let state: HeroTargetState = [modifier] + XCTAssertEqual(state.position, CGPoint(x: 100, y: 200)) + } + + func testPositionWithInvalidValues() { + let nanModifier = HeroModifier.position(CGPoint(x: CGFloat.nan, y: 200)) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.position, "Position should not be set when value is NaN") + + let infModifier = HeroModifier.position(CGPoint(x: CGFloat.infinity, y: 200)) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.position, "Position should not be set when value is Infinity") + } + + // MARK: - Size Modifier Tests + + func testSizeWithValidValues() { + let modifier = HeroModifier.size(CGSize(width: 100, height: 200)) + let state: HeroTargetState = [modifier] + XCTAssertEqual(state.size, CGSize(width: 100, height: 200)) + } + + func testSizeWithInvalidValues() { + let nanModifier = HeroModifier.size(CGSize(width: CGFloat.nan, height: 200)) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.size, "Size should not be set when value is NaN") + + let infModifier = HeroModifier.size(CGSize(width: 100, height: CGFloat.infinity)) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.size, "Size should not be set when value is Infinity") + } + + // MARK: - Perspective Modifier Tests + + func testPerspectiveWithValidValue() { + let modifier = HeroModifier.perspective(500) + let state: HeroTargetState = [modifier] + XCTAssertNotNil(state.transform, "Transform should be set with valid perspective") + if let m34 = state.transform?.m34 { + XCTAssertEqual(Double(m34), 1.0 / -500.0, accuracy: 0.0001) + } + } + + func testPerspectiveWithInvalidValues() { + let zeroModifier = HeroModifier.perspective(0) + let zeroState: HeroTargetState = [zeroModifier] + XCTAssertNil(zeroState.transform, "Transform should not be set when perspective is zero") + + let nanModifier = HeroModifier.perspective(CGFloat.nan) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.transform, "Transform should not be set when perspective is NaN") + + let infModifier = HeroModifier.perspective(CGFloat.infinity) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.transform, "Transform should not be set when perspective is Infinity") + } + + // MARK: - Scale Modifier Tests + + func testScaleWithValidValues() { + let modifier = HeroModifier.scale(x: 2, y: 3, z: 1) + let state: HeroTargetState = [modifier] + XCTAssertNotNil(state.transform, "Transform should be set with valid scale values") + } + + func testScaleWithInvalidValues() { + let nanModifier = HeroModifier.scale(x: CGFloat.nan, y: 1, z: 1) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.transform, "Transform should not be set when value is NaN") + + let infModifier = HeroModifier.scale(x: 1, y: CGFloat.infinity, z: 1) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.transform, "Transform should not be set when value is Infinity") + } + + // MARK: - Translate Modifier Tests + + func testTranslateWithValidValues() { + let modifier = HeroModifier.translate(x: 50, y: 100, z: 0) + let state: HeroTargetState = [modifier] + XCTAssertNotNil(state.transform, "Transform should be set with valid translate values") + } + + func testTranslateWithInvalidValues() { + let nanModifier = HeroModifier.translate(x: 50, y: 100, z: CGFloat.nan) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.transform, "Transform should not be set when value is NaN") + + let infModifier = HeroModifier.translate(x: CGFloat.infinity, y: 100, z: 0) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.transform, "Transform should not be set when value is Infinity") + } + + // MARK: - Rotate Modifier Tests + + func testRotateWithValidValues() { + let modifier = HeroModifier.rotate(x: 0, y: 0, z: .pi) + let state: HeroTargetState = [modifier] + XCTAssertNotNil(state.transform, "Transform should be set with valid rotate values") + } + + func testRotateWithInvalidValues() { + let nanModifier = HeroModifier.rotate(x: CGFloat.nan, y: 0, z: 0) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.transform, "Transform should not be set when value is NaN") + + let infModifier = HeroModifier.rotate(x: 0, y: 0, z: CGFloat.infinity) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.transform, "Transform should not be set when value is Infinity") + } + + // MARK: - Duration Modifier Tests + + func testDurationWithValidValues() { + let validModifier = HeroModifier.duration(0.5) + let validState: HeroTargetState = [validModifier] + XCTAssertEqual(validState.duration, 0.5) + + let infModifier = HeroModifier.duration(.infinity) + let infState: HeroTargetState = [infModifier] + XCTAssertEqual(infState.duration, .infinity, "Infinity should be allowed for duration") + } + + func testDurationWithInvalidValues() { + let nanModifier = HeroModifier.duration(TimeInterval.nan) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.duration, "Duration should not be set when value is NaN") + + let negativeModifier = HeroModifier.duration(-1.0) + let negativeState: HeroTargetState = [negativeModifier] + XCTAssertNil(negativeState.duration, "Duration should not be set when value is negative") + } + + // MARK: - Delay Modifier Tests + + func testDelayWithValidValue() { + let modifier = HeroModifier.delay(0.3) + let state: HeroTargetState = [modifier] + XCTAssertEqual(state.delay, 0.3) + } + + func testDelayWithInvalidValues() { + let nanModifier = HeroModifier.delay(TimeInterval.nan) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.delay, "Delay should not be set when value is NaN") + + let infModifier = HeroModifier.delay(TimeInterval.infinity) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.delay, "Delay should not be set when value is Infinity") + + let negativeModifier = HeroModifier.delay(-0.5) + let negativeState: HeroTargetState = [negativeModifier] + XCTAssertNil(negativeState.delay, "Delay should not be set when value is negative") + } + + // MARK: - Arc Modifier Tests + + func testArcWithValidIntensity() { + let modifier = HeroModifier.arc(intensity: 1) + let state: HeroTargetState = [modifier] + XCTAssertEqual(state.arc, 1) + } + + func testArcWithInvalidValues() { + let nanModifier = HeroModifier.arc(intensity: CGFloat.nan) + let nanState: HeroTargetState = [nanModifier] + XCTAssertNil(nanState.arc, "Arc should not be set when intensity is NaN") + + let infModifier = HeroModifier.arc(intensity: CGFloat.infinity) + let infState: HeroTargetState = [infModifier] + XCTAssertNil(infState.arc, "Arc should not be set when intensity is Infinity") + } +} +#endif