diff --git a/HammerTests.podspec b/HammerTests.podspec index 30ad008..b46a714 100644 --- a/HammerTests.podspec +++ b/HammerTests.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "HammerTests" - spec.version = "0.14.2" + spec.version = "0.14.3" spec.summary = "iOS touch and keyboard syntheis library for unit tests." spec.description = "Hammer is a touch and keyboard synthesis library for emulating user interaction events. It enables new ways of triggering UI actions in unit tests, replicating a real world environment as much as possible." spec.homepage = "https://github.com/lyft/Hammer" diff --git a/Sources/Hammer/EventGenerator/EventGenerator.swift b/Sources/Hammer/EventGenerator/EventGenerator.swift index 056d5d5..39767d2 100644 --- a/Sources/Hammer/EventGenerator/EventGenerator.swift +++ b/Sources/Hammer/EventGenerator/EventGenerator.swift @@ -22,7 +22,7 @@ public final class EventGenerator { public let window: UIWindow /// The view that was used to create the event generator - private(set) var mainView: UIView + public private(set) var mainView: UIView var activeTouches = TouchStorage() var debugWindow = DebugVisualizerWindow() diff --git a/Sources/Hammer/EventGenerator/HammerLocatable.swift b/Sources/Hammer/EventGenerator/HammerLocatable.swift index b8672b9..d6be92a 100644 --- a/Sources/Hammer/EventGenerator/HammerLocatable.swift +++ b/Sources/Hammer/EventGenerator/HammerLocatable.swift @@ -33,3 +33,57 @@ extension String: HammerLocatable { return try eventGenerator.viewWithIdentifier(self).windowHitPoint(for: eventGenerator) } } + +/// Creates an absolute offset for a location in screen points. +public struct OffsetLocation: HammerLocatable { + public let location: HammerLocatable? + public let x: CGFloat + public let y: CGFloat + + /// Creates an offset for a location. + /// + /// - parameter location: The location to offset. Passing nil will use the default location. + /// - parameter x: The x offset. + /// - parameter y: The y offset. + public init(location: HammerLocatable? = nil, x: CGFloat, y: CGFloat) { + self.location = location + self.x = x + self.y = y + } + + public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint { + let location = self.location ?? eventGenerator.mainView + let hitPoint = try location.windowHitPoint(for: eventGenerator) + return CGPoint(x: hitPoint.x + self.x, + y: hitPoint.y + self.y) + } +} + +/// Creates a relative location for a view. +public struct RelativeLocation: HammerLocatable { + public let view: UIView? + public let x: CGFloat + public let y: CGFloat + + /// Creates a relative location for a view + /// + /// Values for x and y are relative to the dimensions of the view. From 0 to 1, 0 being the top/left of + /// the view and 1 being the bottom/right of the view. Passing a value outside those bounds will result + /// in the touch occurring outside the view. + /// + /// - parameter view: The view to get a relative location for. Passing nil will use the default view. + /// - parameter x: The relative x value. + /// - parameter y: The relative y value. + public init(location view: UIView? = nil, x: CGFloat, y: CGFloat) { + self.view = view + self.x = x + self.y = y + } + + public func windowHitPoint(for eventGenerator: EventGenerator) throws -> CGPoint { + let view = self.view ?? eventGenerator.mainView + let hitPoint = try eventGenerator.windowHitPoint(forView: view) + return CGPoint(x: hitPoint.x - view.bounds.center.x + view.bounds.width * self.x, + y: hitPoint.y - view.bounds.center.y + view.bounds.height * self.y) + } +} diff --git a/Tests/HammerTests/HandTests.swift b/Tests/HammerTests/HandTests.swift index 941b00b..007d2fb 100644 --- a/Tests/HammerTests/HandTests.swift +++ b/Tests/HammerTests/HandTests.swift @@ -358,4 +358,56 @@ final class HandTests: XCTestCase { try eventGenerator.wait(0.3) XCTAssertEqual(view.zoomScale, 1, accuracy: 0.1) } + + func testOffsetLocation() throws { + let view = UIStackView() + view.axis = .horizontal + + var expectations = [XCTestExpectation]() + + for i in 1...3 { + let button = UIButton() + button.setTitle("\(i)", for: .normal) + button.setSize(width: 100, height: 100) + let expectation = XCTestExpectation(description: "Button Tapped") + expectation.assertForOverFulfill = true + button.addHandler(forEvent: .primaryActionTriggered, action: expectation.fulfill) + expectations.append(expectation) + view.addArrangedSubview(button) + } + + let eventGenerator = try EventGenerator(view: view) + try eventGenerator.waitUntilHittable(view.subviews[0], timeout: 1) + try eventGenerator.fingerTap(at: OffsetLocation(x: -100, y: 0)) + try eventGenerator.fingerTap(at: OffsetLocation(x: 0, y: 0)) + try eventGenerator.fingerTap(at: OffsetLocation(x: 100, y: 0)) + + XCTAssertEqual(XCTWaiter.wait(for: expectations, timeout: 1, enforceOrder: true), .completed) + } + + func testRelativeLocation() throws { + let view = UIStackView() + view.axis = .horizontal + + var expectations = [XCTestExpectation]() + + for i in 1...3 { + let button = UIButton() + button.setTitle("\(i)", for: .normal) + button.setSize(width: 100, height: 100) + let expectation = XCTestExpectation(description: "Button Tapped") + expectation.assertForOverFulfill = true + button.addHandler(forEvent: .primaryActionTriggered, action: expectation.fulfill) + expectations.append(expectation) + view.addArrangedSubview(button) + } + + let eventGenerator = try EventGenerator(view: view) + try eventGenerator.waitUntilHittable(view.subviews[0], timeout: 1) + try eventGenerator.fingerTap(at: RelativeLocation(x: 0.2, y: 0.5)) + try eventGenerator.fingerTap(at: RelativeLocation(x: 0.5, y: 0.5)) + try eventGenerator.fingerTap(at: RelativeLocation(x: 0.8, y: 0.5)) + + XCTAssertEqual(XCTWaiter.wait(for: expectations, timeout: 1, enforceOrder: true), .completed) + } }