Skip to content

Commit 90af16d

Browse files
Wait for a frame to render before emitting events (#64)
This should help reduce some flakiness, specially when the simulator is taking some time to launch --------- Signed-off-by: Gabriel Lanata <[email protected]>
1 parent 340ac77 commit 90af16d

File tree

4 files changed

+71
-16
lines changed

4 files changed

+71
-16
lines changed

Sources/Hammer/EventGenerator/EventGenerator+Settings.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ extension EventGenerator {
1818

1919
/// If we should wait for animations to complete when an event generator is created.
2020
public var waitForAnimations: Bool = false
21+
22+
/// If we should wait for a frame to complete rendering when an event generator is created.
23+
public var waitForFrameRender: Bool = true
2124
}
2225
}

Sources/Hammer/EventGenerator/EventGenerator.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ public final class EventGenerator {
116116
try self.waitUntil(self.isWindowReady, timeout: timeout)
117117
try self.waitUntilAccessibilityActivate()
118118

119+
if EventGenerator.settings.waitForFrameRender {
120+
try self.waitUntilFrameIsRendered(timeout: timeout)
121+
}
122+
119123
if EventGenerator.settings.waitForAnimations {
120124
try self.waitUntilAnimationsAreFinished(timeout: timeout)
121125
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import QuartzCore
2+
3+
// Singleton class that helps detect frame renders
4+
final class FrameTracker {
5+
static let shared = FrameTracker()
6+
7+
private var displayLink: CADisplayLink?
8+
private var listeners: [() -> Void] = []
9+
10+
private init() {
11+
self.displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback))
12+
self.displayLink?.add(to: .main, forMode: .common)
13+
}
14+
15+
/// Adds a listener that will be called on the next frame render. Will only be called once
16+
///
17+
/// - parameter listener: The listener to call on the next frame render
18+
func addNextFrameListener(_ listener: @escaping () -> Void) {
19+
self.listeners.append(listener)
20+
}
21+
22+
deinit {
23+
self.displayLink?.invalidate()
24+
}
25+
26+
// MARK: - Private Methods
27+
28+
@objc
29+
private func displayLinkCallback() {
30+
self.listeners.forEach { $0() }
31+
self.listeners.removeAll(keepingCapacity: true)
32+
}
33+
}

Sources/Hammer/Utilties/Waiting.swift

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@ extension EventGenerator {
2929
}
3030

3131
/// Begin waiting
32-
public func start() throws {
32+
///
33+
/// - parameter throwIfAlreadyCompleted: If true, throws an error if the waiter has already completed
34+
public func start(throwIfAlreadyCompleted: Bool = true) throws {
3335
if case .running = self.state {
3436
throw HammerError.waiterIsAlreadyRunning
3537
} else if case .completed = self.state {
36-
throw HammerError.waiterIsAlreadyCompleted
38+
if throwIfAlreadyCompleted {
39+
throw HammerError.waiterIsAlreadyCompleted
40+
} else {
41+
return
42+
}
3743
}
3844

3945
self.state = .running
@@ -67,6 +73,18 @@ extension EventGenerator {
6773
try Waiter(timeout: interval).start()
6874
}
6975

76+
/// Waits for the condition closure to call complete on the waiter.
77+
///
78+
/// - parameter condition: The condition to check.
79+
/// - parameter timeout: The maximum time to wait for the condition to complete.
80+
///
81+
/// - throws: An error if the condition did not complete within the specified time.
82+
public func waitUntil(_ condition: @escaping (Waiter) throws -> Void, timeout: TimeInterval) throws {
83+
let waiter = Waiter(timeout: timeout)
84+
try condition(waiter)
85+
try waiter.start(throwIfAlreadyCompleted: false)
86+
}
87+
7088
/// Waits for a condition to become true within the specified time.
7189
///
7290
/// - parameter condition: The condition to check.
@@ -248,20 +266,17 @@ extension EventGenerator {
248266
///
249267
/// - throws: An error if the runloop is not flushed within the specified time.
250268
public func waitUntilRunloopIsFlushed(timeout: TimeInterval) throws {
251-
var errorCompleting: Error?
252-
253-
let waiter = Waiter(timeout: timeout)
254-
DispatchQueue.main.async {
255-
do {
256-
try waiter.complete()
257-
} catch {
258-
errorCompleting = error
259-
}
260-
}
269+
try self.waitUntil({ waiter in
270+
DispatchQueue.main.async { try? waiter.complete() }
271+
}, timeout: timeout)
272+
}
261273

262-
try waiter.start()
263-
if let errorCompleting {
264-
throw errorCompleting
265-
}
274+
/// Waits until a frame has completed rendering.
275+
///
276+
/// - parameter timeout: The maximum time to wait for the frame to render.
277+
public func waitUntilFrameIsRendered(timeout: TimeInterval) throws {
278+
try self.waitUntil({ waiter in
279+
FrameTracker.shared.addNextFrameListener { try? waiter.complete() }
280+
}, timeout: timeout)
266281
}
267282
}

0 commit comments

Comments
 (0)