diff --git a/Sources/Hammer/EventGenerator/EventGenerator+Settings.swift b/Sources/Hammer/EventGenerator/EventGenerator+Settings.swift index c477979..ace7564 100644 --- a/Sources/Hammer/EventGenerator/EventGenerator+Settings.swift +++ b/Sources/Hammer/EventGenerator/EventGenerator+Settings.swift @@ -18,5 +18,8 @@ extension EventGenerator { /// If we should wait for animations to complete when an event generator is created. public var waitForAnimations: Bool = false + + /// If we should wait for a frame to complete rendering when an event generator is created. + public var waitForFrameRender: Bool = true } } diff --git a/Sources/Hammer/EventGenerator/EventGenerator.swift b/Sources/Hammer/EventGenerator/EventGenerator.swift index d3243d7..2224366 100644 --- a/Sources/Hammer/EventGenerator/EventGenerator.swift +++ b/Sources/Hammer/EventGenerator/EventGenerator.swift @@ -116,6 +116,10 @@ public final class EventGenerator { try self.waitUntil(self.isWindowReady, timeout: timeout) try self.waitUntilAccessibilityActivate() + if EventGenerator.settings.waitForFrameRender { + try self.waitUntilFrameIsRendered(timeout: timeout) + } + if EventGenerator.settings.waitForAnimations { try self.waitUntilAnimationsAreFinished(timeout: timeout) } diff --git a/Sources/Hammer/Utilties/FrameTracker.swift b/Sources/Hammer/Utilties/FrameTracker.swift new file mode 100644 index 0000000..7f12785 --- /dev/null +++ b/Sources/Hammer/Utilties/FrameTracker.swift @@ -0,0 +1,33 @@ +import QuartzCore + +// Singleton class that helps detect frame renders +final class FrameTracker { + static let shared = FrameTracker() + + private var displayLink: CADisplayLink? + private var listeners: [() -> Void] = [] + + private init() { + self.displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback)) + self.displayLink?.add(to: .main, forMode: .common) + } + + /// Adds a listener that will be called on the next frame render. Will only be called once + /// + /// - parameter listener: The listener to call on the next frame render + func addNextFrameListener(_ listener: @escaping () -> Void) { + self.listeners.append(listener) + } + + deinit { + self.displayLink?.invalidate() + } + + // MARK: - Private Methods + + @objc + private func displayLinkCallback() { + self.listeners.forEach { $0() } + self.listeners.removeAll(keepingCapacity: true) + } +} diff --git a/Sources/Hammer/Utilties/Waiting.swift b/Sources/Hammer/Utilties/Waiting.swift index 7339e2d..ecd2615 100644 --- a/Sources/Hammer/Utilties/Waiting.swift +++ b/Sources/Hammer/Utilties/Waiting.swift @@ -29,11 +29,17 @@ extension EventGenerator { } /// Begin waiting - public func start() throws { + /// + /// - parameter throwIfAlreadyCompleted: If true, throws an error if the waiter has already completed + public func start(throwIfAlreadyCompleted: Bool = true) throws { if case .running = self.state { throw HammerError.waiterIsAlreadyRunning } else if case .completed = self.state { - throw HammerError.waiterIsAlreadyCompleted + if throwIfAlreadyCompleted { + throw HammerError.waiterIsAlreadyCompleted + } else { + return + } } self.state = .running @@ -67,6 +73,18 @@ extension EventGenerator { try Waiter(timeout: interval).start() } + /// Waits for the condition closure to call complete on the waiter. + /// + /// - parameter condition: The condition to check. + /// - parameter timeout: The maximum time to wait for the condition to complete. + /// + /// - throws: An error if the condition did not complete within the specified time. + public func waitUntil(_ condition: @escaping (Waiter) throws -> Void, timeout: TimeInterval) throws { + let waiter = Waiter(timeout: timeout) + try condition(waiter) + try waiter.start(throwIfAlreadyCompleted: false) + } + /// Waits for a condition to become true within the specified time. /// /// - parameter condition: The condition to check. @@ -248,20 +266,17 @@ extension EventGenerator { /// /// - throws: An error if the runloop is not flushed within the specified time. public func waitUntilRunloopIsFlushed(timeout: TimeInterval) throws { - var errorCompleting: Error? - - let waiter = Waiter(timeout: timeout) - DispatchQueue.main.async { - do { - try waiter.complete() - } catch { - errorCompleting = error - } - } + try self.waitUntil({ waiter in + DispatchQueue.main.async { try? waiter.complete() } + }, timeout: timeout) + } - try waiter.start() - if let errorCompleting { - throw errorCompleting - } + /// Waits until a frame has completed rendering. + /// + /// - parameter timeout: The maximum time to wait for the frame to render. + public func waitUntilFrameIsRendered(timeout: TimeInterval) throws { + try self.waitUntil({ waiter in + FrameTracker.shared.addNextFrameListener { try? waiter.complete() } + }, timeout: timeout) } }