From 0a07dd6c73d7316e5e42622d43276d8916e03d12 Mon Sep 17 00:00:00 2001 From: Gabriel Lanata Date: Thu, 29 Jul 2021 18:27:50 -0700 Subject: [PATCH] Improve waiting methods (#26) --- HammerTests.podspec | 2 +- README.md | 4 +- .../EventGenerator/EventGenerator.swift | 8 ++- Sources/Hammer/Utilties/HammerError.swift | 10 ++++ Sources/Hammer/Utilties/Waiting.swift | 58 ++++++++++++++++++- Tests/HammerTests/KeyboardTests.swift | 13 +++-- project.yml | 2 + 7 files changed, 85 insertions(+), 12 deletions(-) diff --git a/HammerTests.podspec b/HammerTests.podspec index 0593773..46a94ed 100644 --- a/HammerTests.podspec +++ b/HammerTests.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "HammerTests" - spec.version = "0.12.0" + spec.version = "0.13.0" 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/README.md b/README.md index e4870aa..68d3420 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ Hammer requires Swift 5.3 and iOS 11.0 or later. #### With [SwiftPM](https://swift.org/package-manager) ```swift -.package(url: "https://github.com/lyft/Hammer.git", from: "0.12.0") +.package(url: "https://github.com/lyft/Hammer.git", from: "0.13.0") ``` #### With [CocoaPods](https://cocoapods.org/) ```ruby -pod 'HammerTests', '~> 0.12.0' +pod 'HammerTests', '~> 0.13.0' ``` ## Setup diff --git a/Sources/Hammer/EventGenerator/EventGenerator.swift b/Sources/Hammer/EventGenerator/EventGenerator.swift index 589b034..a489a83 100644 --- a/Sources/Hammer/EventGenerator/EventGenerator.swift +++ b/Sources/Hammer/EventGenerator/EventGenerator.swift @@ -53,6 +53,8 @@ public final class EventGenerator { UIApplication.registerForHIDEvents(ObjectIdentifier(self)) { [weak self] event in self?.markerEventReceived(event) } + + try self.waitUntilWindowIsReady() } /// Initialize an event generator for a specified UIViewController. @@ -178,8 +180,8 @@ public final class EventGenerator { /// Sleeps the current thread until the events have finished sending. private func waitForEvents() throws { - let runLoop = CFRunLoopGetCurrent() - try self.sendMarkerEvent { CFRunLoopStop(runLoop) } - CFRunLoopRun() + let waiter = Waiter(timeout: 1) + try self.sendMarkerEvent { try? waiter.complete() } + try waiter.start() } } diff --git a/Sources/Hammer/Utilties/HammerError.swift b/Sources/Hammer/Utilties/HammerError.swift index 588c82a..9add251 100644 --- a/Sources/Hammer/Utilties/HammerError.swift +++ b/Sources/Hammer/Utilties/HammerError.swift @@ -30,6 +30,10 @@ public enum HammerError: Error { case unableToFindView(identifier: String) case invalidViewType(identifier: String, type: String, expected: String) case waitConditionTimeout(TimeInterval) + + case waiterIsNotRunning + case waiterIsAlreadyRunning + case waiterIsAlreadyCompleted } extension HammerError: CustomStringConvertible { @@ -79,6 +83,12 @@ extension HammerError: CustomStringConvertible { return "Invalid type for view: \"\(identifier)\", got \"\(type)\" expected \"\(expected)\"" case .waitConditionTimeout(let timeout): return "Timeout while waiting for condition exceeded \(timeout) seconds" + case .waiterIsNotRunning: + return "Unable to stop a Waiter that is not running" + case .waiterIsAlreadyRunning: + return "Unable to start a Waiter that is already running" + case .waiterIsAlreadyCompleted: + return "Unable to start or stop a waiter that is already completed" } } } diff --git a/Sources/Hammer/Utilties/Waiting.swift b/Sources/Hammer/Utilties/Waiting.swift index c1bd68b..9f10ad8 100644 --- a/Sources/Hammer/Utilties/Waiting.swift +++ b/Sources/Hammer/Utilties/Waiting.swift @@ -1,14 +1,70 @@ import Foundation import UIKit +import XCTest extension EventGenerator { + /// Object to handle waiting + public final class Waiter { + public enum State { + case idle + case running + case completed(timeout: Bool) + } + + /// The maximum time to wait before stopping itself + public let timeout: TimeInterval + + /// The current state of the waiter + public private(set) var state: State = .idle + + /// We use XCTestExpectations internally to sleep the execution in a way that is friendly to tests + /// and does not block the main thread. + private let expectation = XCTestExpectation(description: "Hammer-Wait") + + /// Initialize a Waiter + /// + /// - parameter timeout: The maximum time to wait before stopping itself + public init(timeout: TimeInterval) { + self.timeout = timeout + } + + /// Begin waiting + public func start() throws { + if case .running = self.state { + throw HammerError.waiterIsAlreadyRunning + } else if case .completed = self.state { + throw HammerError.waiterIsAlreadyCompleted + } + + self.state = .running + let result = XCTWaiter.wait(for: [self.expectation], timeout: self.timeout) + switch result { + case .completed: + self.state = .completed(timeout: false) + default: + self.state = .completed(timeout: true) + } + } + + /// Stop waiting before the timeout + public func complete() throws { + if case .idle = self.state { + throw HammerError.waiterIsNotRunning + } else if case .completed = self.state { + throw HammerError.waiterIsAlreadyCompleted + } + + self.expectation.fulfill() + } + } + /// Waits for a specified time. /// /// - parameter interval: The maximum time to wait. /// /// - throws: An error if there was an issue during waiting. public func wait(_ interval: TimeInterval) throws { - CFRunLoopRunInMode(CFRunLoopMode.defaultMode, interval, false) + try Waiter(timeout: interval).start() } /// Waits for a condition to become true within the specified time. diff --git a/Tests/HammerTests/KeyboardTests.swift b/Tests/HammerTests/KeyboardTests.swift index e758f27..84458a6 100644 --- a/Tests/HammerTests/KeyboardTests.swift +++ b/Tests/HammerTests/KeyboardTests.swift @@ -171,15 +171,18 @@ final class KeyboardTests: XCTestCase { view.autocapitalizationType = .none view.widthAnchor.constraint(equalToConstant: 300).isActive = true - let window = UIWindow(frame: UIScreen.main.bounds) - window.isHidden = false - window.addSubview(view) + let viewController = UIViewController() + viewController.view.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - view.centerYAnchor.constraint(equalTo: window.centerYAnchor), - view.centerXAnchor.constraint(equalTo: window.centerXAnchor), + view.centerYAnchor.constraint(equalTo: viewController.view.centerYAnchor), + view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor), ]) + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = viewController + window.isHidden = false + let eventGenerator = try EventGenerator(window: window) try eventGenerator.waitUntilHittable(timeout: 1) diff --git a/project.yml b/project.yml index 2fceeb7..3efd2d5 100644 --- a/project.yml +++ b/project.yml @@ -7,6 +7,8 @@ targets: platform: iOS deploymentTarget: "11.0" sources: Sources/Hammer + settings: + ENABLE_TESTING_SEARCH_PATHS: true scheme: testTargets: - HammerTests