Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/Hammer/EventGenerator/EventGenerator+Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
4 changes: 4 additions & 0 deletions Sources/Hammer/EventGenerator/EventGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
33 changes: 33 additions & 0 deletions Sources/Hammer/Utilties/FrameTracker.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
47 changes: 31 additions & 16 deletions Sources/Hammer/Utilties/Waiting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
}
Loading