import Testing import CoreGraphics @testable import TVBox /// Property-based tests for PlayerGestureLayer.classifyGesture zone classification. /// These tests validate that gesture classification is deterministic and mutually exclusive. /// /// **Validates: Requirements 1.1, 2.1, 3.1** // MARK: - Property 2: Gesture zone classification is deterministic and mutually exclusive /// For any touch start position x within a container of width w > 0, and any drag translation /// (dx, dy) exceeding the threshold, the gesture classification function SHALL return exactly /// one of: .seeking, .adjustingBrightness, or .adjustingVolume, where brightness is selected /// iff x < w/2 and |dy| > |dx|, and volume is selected iff x >= w/2 and |dy| > |dx|. @Suite("Property 2: Gesture zone classification is deterministic and mutually exclusive") struct GestureZoneClassificationPropertyTests { static let threshold: CGFloat = 10.0 // MARK: - Determinism: same inputs always produce same output @Test("Classification is deterministic - same inputs always produce same result") func classificationIsDeterministic() { for _ in 0..<500 { let containerWidth = CGFloat.random(in: 1.0...2000.0) let startX = CGFloat.random(in: 0.0.. |dy| and |dx| > threshold → seeking @Test("Horizontal gesture returns .seeking when |dx| > |dy| and |dx| > threshold") func horizontalGestureReturnsSeeking() { for _ in 0..<500 { let containerWidth = CGFloat.random(in: 1.0...2000.0) let startX = CGFloat.random(in: 0.0.. |dy|=\(abs(dy)) > threshold, got \(result)") } } } // MARK: - Left half vertical: |dy| > |dx| and |dy| > threshold and x < w/2 → brightness @Test("Left half vertical gesture returns .adjustingBrightness when |dy| > |dx| and startX < w/2") func leftHalfVerticalReturnsBrightness() { for _ in 0..<500 { let containerWidth = CGFloat.random(in: 2.0...2000.0) // startX strictly in left half let startX = CGFloat.random(in: 0.0..<(containerWidth / 2.0)) // Generate dy that exceeds threshold let absDy = CGFloat.random(in: (Self.threshold + 1)...1000.0) let dy = Bool.random() ? absDy : -absDy // Generate dx with |dx| < |dy| to ensure vertical dominance // Also ensure |dx| <= |dy| so the first condition (absH > absV) is false let absDx = CGFloat.random(in: 0.0...absDy) let dx = Bool.random() ? absDx : -absDx let translation = CGSize(width: dx, height: dy) let result = PlayerGestureLayer.classifyGesture( startX: startX, containerWidth: containerWidth, translation: translation, threshold: Self.threshold ) if case .adjustingBrightness = result { // Expected } else { Issue.record("Expected .adjustingBrightness for startX=\(startX) < w/2=\(containerWidth/2), |dy|=\(abs(dy)) > |dx|=\(abs(dx)), got \(result)") } } } // MARK: - Right half vertical: |dy| > |dx| and |dy| > threshold and x >= w/2 → volume @Test("Right half vertical gesture returns .adjustingVolume when |dy| > |dx| and startX >= w/2") func rightHalfVerticalReturnsVolume() { for _ in 0..<500 { let containerWidth = CGFloat.random(in: 2.0...2000.0) // startX in right half (>= w/2) let startX = CGFloat.random(in: (containerWidth / 2.0)...containerWidth) // Generate dy that exceeds threshold let absDy = CGFloat.random(in: (Self.threshold + 1)...1000.0) let dy = Bool.random() ? absDy : -absDy // Generate dx with |dx| < |dy| to ensure vertical dominance let absDx = CGFloat.random(in: 0.0...absDy) let dx = Bool.random() ? absDx : -absDx let translation = CGSize(width: dx, height: dy) let result = PlayerGestureLayer.classifyGesture( startX: startX, containerWidth: containerWidth, translation: translation, threshold: Self.threshold ) if case .adjustingVolume = result { // Expected } else { Issue.record("Expected .adjustingVolume for startX=\(startX) >= w/2=\(containerWidth/2), |dy|=\(abs(dy)) > |dx|=\(abs(dx)), got \(result)") } } } // MARK: - Below threshold: neither exceeds threshold → .none @Test("Returns .none when neither component exceeds threshold") func belowThresholdReturnsNone() { for _ in 0..<500 { let containerWidth = CGFloat.random(in: 1.0...2000.0) let startX = CGFloat.random(in: 0.0..= w/2 means right half → volume } else { Issue.record("Expected .adjustingVolume at exact midpoint startX=\(startX), w=\(containerWidth), got \(result)") } } } // MARK: - Comprehensive zone correctness with random inputs @Test("Zone classification matches specification for all random inputs") func zoneClassificationMatchesSpec() { for _ in 0..<1000 { let containerWidth = CGFloat.random(in: 1.0...2000.0) let startX = CGFloat.random(in: 0.0.. absV && absH > Self.threshold { // Should be seeking if case .seeking = result { // Correct } else { Issue.record("Expected .seeking when |dx|=\(absH) > |dy|=\(absV) and |dx| > threshold, got \(result)") } } else if absV > Self.threshold { // Should be brightness or volume based on position let isLeftHalf = startX < containerWidth / 2.0 if isLeftHalf { if case .adjustingBrightness = result { // Correct } else { Issue.record("Expected .adjustingBrightness for left half (startX=\(startX) < w/2=\(containerWidth/2)), got \(result)") } } else { if case .adjustingVolume = result { // Correct } else { Issue.record("Expected .adjustingVolume for right half (startX=\(startX) >= w/2=\(containerWidth/2)), got \(result)") } } } else { // Neither exceeds threshold → .none #expect(result == .none, "Expected .none when neither exceeds threshold (|dx|=\(absH), |dy|=\(absV)), got \(result)") } } } }