diff --git a/Sources/WebDriver/Element.swift b/Sources/WebDriver/Element.swift index 22f2b94..712bb6f 100644 --- a/Sources/WebDriver/Element.swift +++ b/Sources/WebDriver/Element.swift @@ -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: @@ -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. diff --git a/Sources/WebDriver/ErrorResponse.swift b/Sources/WebDriver/ErrorResponse.swift index 3bd0109..8d24380 100644 --- a/Sources/WebDriver/ErrorResponse.swift +++ b/Sources/WebDriver/ErrorResponse.swift @@ -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 } diff --git a/Sources/WebDriver/Session.swift b/Sources/WebDriver/Session.swift index 69b6a0b..200e170 100644 --- a/Sources/WebDriver/Session.swift +++ b/Sources/WebDriver/Session.swift @@ -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(_ 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 { diff --git a/Sources/WebDriver/WebDriver.swift b/Sources/WebDriver/WebDriver.swift index 5ce3408..f7e644d 100644 --- a/Sources/WebDriver/WebDriver.swift +++ b/Sources/WebDriver/WebDriver.swift @@ -1,6 +1,9 @@ public protocol WebDriver { @discardableResult func send(_ request: Req) throws -> Req.Response + + /// Determines if a given error is inconclusive and should be retried. + func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool } extension WebDriver { @@ -9,4 +12,6 @@ extension WebDriver { public var status: WebDriverStatus { get throws { try send(Requests.Status()) } } + + public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool { false } } \ No newline at end of file diff --git a/Sources/WinAppDriver/ErrorResponse+WinAppDriver.swift b/Sources/WinAppDriver/ErrorResponse+WinAppDriver.swift index 7f49d8f..a5b415b 100644 --- a/Sources/WinAppDriver/ErrorResponse+WinAppDriver.swift +++ b/Sources/WinAppDriver/ErrorResponse+WinAppDriver.swift @@ -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) } diff --git a/Sources/WinAppDriver/WinAppDriver.swift b/Sources/WinAppDriver/WinAppDriver.swift index 6e92420..2be1566 100644 --- a/Sources/WinAppDriver/WinAppDriver.swift +++ b/Sources/WinAppDriver/WinAppDriver.swift @@ -72,4 +72,8 @@ public class WinAppDriver: WebDriver { public func send(_ request: Req) throws -> Req.Response { try httpWebDriver.send(request) } + + public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool { + error == .winAppDriver_elementNotInteractable || httpWebDriver.isInconclusiveInteraction(error: error) + } }