Skip to content

Commit

Permalink
Abstract retries on inconclusive interactions (#150)
Browse files Browse the repository at this point in the history
- Moves the last WinAppDriver-specific status code to that module
- Consolidate the polling code that checks for elementNotInteractable
  • Loading branch information
tristanlabelle authored May 6, 2024
1 parent 4555637 commit 084d1ef
Show file tree
Hide file tree
Showing 6 changed files with 31 additions and 50 deletions.
48 changes: 4 additions & 44 deletions Sources/WebDriver/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,49 +56,19 @@ public struct Element {
/// Clicks this element.
public func click(retryTimeout: TimeInterval? = nil) throws {
let request = Requests.ElementClick(session: session.id, element: id)
let result = try poll(timeout: retryTimeout ?? session.defaultRetryTimeout) {
do {
// Immediately bubble most failures, only retry on element not interactable.
try webDriver.send(request)
return PollResult.success(nil as ErrorResponse?)
} catch let error as ErrorResponse where error.status == .winAppDriver_elementNotInteractable {
return PollResult.failure(error)
}
}

if let notInteractableError = result.value { throw notInteractableError }
try session.sendInteraction(request, retryTimeout: retryTimeout)
}

/// Clicks this element via touch.
public func touchClick(kind: TouchClickKind = .single, retryTimeout: TimeInterval? = nil) throws {
let request = Requests.SessionTouchClick(session: session.id, kind: kind, element: id)
let result = try poll(timeout: retryTimeout ?? session.defaultRetryTimeout) {
do {
// Immediately bubble most failures, only retry on element not interactable.
try webDriver.send(request)
return PollResult.success(nil as ErrorResponse?)
} catch let error as ErrorResponse where error.status == .winAppDriver_elementNotInteractable {
return PollResult.failure(error)
}
}

if let notInteractableError = result.value { throw notInteractableError }
try session.sendInteraction(request, retryTimeout: retryTimeout)
}

/// Double clicks an element by id.
public func doubleClick(retryTimeout: TimeInterval? = nil) throws {
let request = Requests.SessionTouchDoubleClick(session: session.id, element: id)
let result = try poll(timeout: retryTimeout ?? session.defaultRetryTimeout) {
do {
// Immediately bubble most failures, only retry on element not interactable.
try webDriver.send(request)
return PollResult.success(nil as ErrorResponse?)
} catch let error as ErrorResponse where error.status == .winAppDriver_elementNotInteractable {
return PollResult.failure(error)
}
}

if let notInteractableError = result.value { throw notInteractableError }
try session.sendInteraction(request, retryTimeout: retryTimeout)
}

/// - Parameters:
Expand All @@ -109,17 +79,7 @@ public struct Element {
/// - speed: The speed in pixels per seconds.
public func flick(xOffset: Double, yOffset: Double, speed: Double, retryTimeout: TimeInterval? = nil) throws {
let request = Requests.SessionTouchFlickElement(session: session.id, element: id, xOffset: xOffset, yOffset: yOffset, speed: speed)
let result = try poll(timeout: retryTimeout ?? session.defaultRetryTimeout) {
do {
// Immediately bubble most failures, only retry on element not interactable.
try webDriver.send(request)
return PollResult.success(nil as ErrorResponse?)
} catch let error as ErrorResponse where error.status == .winAppDriver_elementNotInteractable {
return PollResult.failure(error)
}
}

if let notInteractableError = result.value { throw notInteractableError }
try session.sendInteraction(request, retryTimeout: retryTimeout)
}

/// Finds an element by id, starting from this element.
Expand Down
5 changes: 0 additions & 5 deletions Sources/WebDriver/ErrorResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ public struct ErrorResponse: Codable, Error {
public static let sessionNotCreatedException = Self(rawValue: 33)
public static let moveTargetOutOfBounds = Self(rawValue: 34)

// WinAppDriver-specific, but we need it in this module,
// as we use it when polling for element clickability.
/// Indicates that an request could not be completed because the element is not pointer- or keyboard interactable.
static let winAppDriver_elementNotInteractable = Self(rawValue: 105)

public init(rawValue: Int) {
self.rawValue = rawValue
}
Expand Down
16 changes: 15 additions & 1 deletion Sources/WebDriver/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,13 +371,27 @@ public class Session {
}

/// Deletes the current session.

public func delete() throws {
guard shouldDelete else { return }
try webDriver.send(Requests.SessionDelete(session: id))
shouldDelete = false
}

/// Sends an interaction request, retrying until it is conclusive or the timeout elapses.
internal func sendInteraction<Req: Request>(_ request: Req, retryTimeout: TimeInterval? = nil) throws where Req.Response == CodableNone {
let result = try poll(timeout: retryTimeout ?? defaultRetryTimeout) {
do {
// Immediately bubble most failures, only retry if inconclusive.
try webDriver.send(request)
return PollResult.success(nil as ErrorResponse?)
} catch let error as ErrorResponse where webDriver.isInconclusiveInteraction(error: error.status) {
return PollResult.failure(error)
}
}

if let error = result.value { throw error }
}

deinit {
do { try delete() }
catch let error as ErrorResponse {
Expand Down
5 changes: 5 additions & 0 deletions Sources/WebDriver/WebDriver.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
public protocol WebDriver {
@discardableResult
func send<Req: Request>(_ request: Req) throws -> Req.Response

/// Determines if a given error is inconclusive and should be retried.
func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool
}

extension WebDriver {
Expand All @@ -9,4 +12,6 @@ extension WebDriver {
public var status: WebDriverStatus {
get throws { try send(Requests.Status()) }
}

public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool { false }
}
3 changes: 3 additions & 0 deletions Sources/WinAppDriver/ErrorResponse+WinAppDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ import WebDriver
extension ErrorResponse.Status {
// WinAppDriver returns when passing an incorrect window handle to attach to.
static let winAppDriver_invalidArgument = Self(rawValue: 100)

/// Indicates that a request could not be completed because the element is not pointer- or keyboard interactable.
static let winAppDriver_elementNotInteractable = Self(rawValue: 105)
}
4 changes: 4 additions & 0 deletions Sources/WinAppDriver/WinAppDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,8 @@ public class WinAppDriver: WebDriver {
public func send<Req: Request>(_ request: Req) throws -> Req.Response {
try httpWebDriver.send(request)
}

public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool {
error == .winAppDriver_elementNotInteractable || httpWebDriver.isInconclusiveInteraction(error: error)
}
}

0 comments on commit 084d1ef

Please sign in to comment.