From 04f6a3c8aaf3b6494e4838d1038db2fda97dfd1d Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Wed, 3 Jul 2024 11:47:32 -0400 Subject: [PATCH 1/3] Leverage WebDriver built-in implicit wait --- Sources/WebDriver/Element.swift | 70 ++++---- Sources/WebDriver/HTTPWebDriver.swift | 4 +- Sources/WebDriver/Session.swift | 176 +++++++++++++-------- Tests/WinAppDriverTests/TimeoutTests.swift | 6 +- 4 files changed, 146 insertions(+), 110 deletions(-) diff --git a/Sources/WebDriver/Element.swift b/Sources/WebDriver/Element.swift index 712bb6f..9cc08d5 100644 --- a/Sources/WebDriver/Element.swift +++ b/Sources/WebDriver/Element.swift @@ -72,7 +72,7 @@ public struct Element { } /// - Parameters: - /// - retryTimeout: Optional value to override defaultRetryTimeout. + /// - retryTimeout: The amount of time to retry the operation. Overrides the implicit interaction retry timeout. /// - element: Element id to click /// - xOffset: The x offset in pixels to flick by. /// - yOffset: The y offset in pixels to flick by. @@ -84,92 +84,92 @@ public struct Element { /// Finds an element by id, starting from this element. /// - Parameter byId: id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The element that was found, if any. - public func findElement(byId id: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(using: "id", value: id, retryTimeout: retryTimeout) + public func findElement(byId id: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(using: "id", value: id, waitTimeout: waitTimeout) } /// Search for an element by name, starting from this element. /// - Parameter byName: name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The element that was found, if any. - public func findElement(byName name: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(using: "name", value: name, retryTimeout: retryTimeout) + public func findElement(byName name: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(using: "name", value: name, waitTimeout: waitTimeout) } /// Search for an element in the accessibility tree, starting from this element. /// - Parameter byAccessibilityId: accessibiilty id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The element that was found, if any. - public func findElement(byAccessibilityId id: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(using: "accessibility id", value: id, retryTimeout: retryTimeout) + public func findElement(byAccessibilityId id: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(using: "accessibility id", value: id, waitTimeout: waitTimeout) } /// Search for an element by xpath, starting from this element. /// - Parameter byXPath: xpath of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: a new instance of Element wrapping the found element, nil if not found. - public func findElement(byXPath xpath: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(using: "xpath", value: xpath, retryTimeout: retryTimeout) + public func findElement(byXPath xpath: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(using: "xpath", value: xpath, waitTimeout: waitTimeout) } /// Search for an element by class name, starting from this element. /// - Parameter byClassName: class name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The element that was found, if any. - public func findElement(byClassName className: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(using: "class name", value: className, retryTimeout: retryTimeout) + public func findElement(byClassName className: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(using: "class name", value: className, waitTimeout: waitTimeout) } // Helper for findElement functions above. - private func findElement(using: String, value: String, retryTimeout: TimeInterval?) throws -> Element? { - try session.findElement(startingAt: self, using: using, value: value, retryTimeout: retryTimeout) + private func findElement(using: String, value: String, waitTimeout: TimeInterval?) throws -> Element? { + try session.findElement(startingAt: self, using: using, value: value, waitTimeout: waitTimeout) } /// Search for elements by id, starting from this element. /// - Parameter byId: id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byId id: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(using: "id", value: id, retryTimeout: retryTimeout) + public func findElements(byId id: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(using: "id", value: id, waitTimeout: waitTimeout) } /// Search for elements by name, starting from this element. /// - Parameter byName: name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byName name: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(using: "name", value: name, retryTimeout: retryTimeout) + public func findElements(byName name: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(using: "name", value: name, waitTimeout: waitTimeout) } /// Search for elements in the accessibility tree, starting from this element. /// - Parameter byAccessibilityId: accessibiilty id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byAccessibilityId id: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(using: "accessibility id", value: id, retryTimeout: retryTimeout) + public func findElements(byAccessibilityId id: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(using: "accessibility id", value: id, waitTimeout: waitTimeout) } /// Search for elements by xpath, starting from this element. /// - Parameter byXPath: xpath of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byXPath xpath: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(using: "xpath", value: xpath, retryTimeout: retryTimeout) + public func findElements(byXPath xpath: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(using: "xpath", value: xpath, waitTimeout: waitTimeout) } /// Search for elements by class name, starting from this element. /// - Parameter byClassName: class name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byClassName className: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(using: "class name", value: className, retryTimeout: retryTimeout) + public func findElements(byClassName className: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(using: "class name", value: className, waitTimeout: waitTimeout) } // Helper for findElements functions above. - private func findElements(using: String, value: String, retryTimeout: TimeInterval?) throws -> [Element] { - try session.findElements(startingAt: self, using: using, value: value, retryTimeout: retryTimeout) + private func findElements(using: String, value: String, waitTimeout: TimeInterval?) throws -> [Element] { + try session.findElements(startingAt: self, using: using, value: value, waitTimeout: waitTimeout) } /// Gets an attribute of this element. diff --git a/Sources/WebDriver/HTTPWebDriver.swift b/Sources/WebDriver/HTTPWebDriver.swift index acd6a81..096dc1e 100644 --- a/Sources/WebDriver/HTTPWebDriver.swift +++ b/Sources/WebDriver/HTTPWebDriver.swift @@ -6,7 +6,7 @@ import FoundationNetworking public struct HTTPWebDriver: WebDriver { let rootURL: URL - public static let defaultTimeout: TimeInterval = 5 // seconds + public static let defaultRequestTimeout: TimeInterval = 5 // seconds public init(endpoint: URL) { rootURL = endpoint @@ -36,7 +36,7 @@ public struct HTTPWebDriver: WebDriver { var urlRequest = URLRequest(url: url) urlRequest.httpMethod = request.method.rawValue // TODO(#40): Setting timeoutInterval causes a crash when sending the request on the CI machines. - // urlRequest.timeoutInterval = Self.defaultTimeout + // urlRequest.timeoutInterval = Self.defaultRequestTimeout // Add the body if the Request type defines one if Req.Body.self != CodableNone.self { diff --git a/Sources/WebDriver/Session.swift b/Sources/WebDriver/Session.swift index 200e170..b199465 100644 --- a/Sources/WebDriver/Session.swift +++ b/Sources/WebDriver/Session.swift @@ -6,28 +6,46 @@ public class Session { public let webDriver: any WebDriver public let id: String public let capabilities: Capabilities + private var _implicitWaitTimeout: TimeInterval = 0 + private var emulateImplicitWait: Bool = false // Set if the session doesn't support implicit waits. private var shouldDelete: Bool = true - public init(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities? = nil) throws { - self.webDriver = webDriver - let response = try webDriver.send(Requests.Session( - desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities)) - self.id = response.sessionId - self.capabilities = response.value - } - public init(webDriver: any WebDriver, existingId: String, capabilities: Capabilities = Capabilities(), owned: Bool = false) { self.webDriver = webDriver self.id = existingId self.capabilities = capabilities + if let implicitWaitTimeoutInMilliseconds = capabilities.timeouts?.implicit { + self.implicitWaitTimeout = Double(implicitWaitTimeoutInMilliseconds) / 1000.0 + } self.shouldDelete = owned } - /// A TimeInterval specifying max time to spend retrying operations. - public var defaultRetryTimeout: TimeInterval = 1.0 { - willSet { precondition(newValue >= 0) } + public convenience init(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities? = nil) throws { + let response = try webDriver.send(Requests.Session( + desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities)) + self.init(webDriver: webDriver, existingId: response.sessionId, capabilities: response.value, owned: true) + } + + /// The amount of time the driver should implicitly wait when searching for elements. + /// This functionality is either implemented by the driver, or emulated by swift-webdriver as a fallback. + public var implicitWaitTimeout: TimeInterval { + get { _implicitWaitTimeout } + set { + if newValue == _implicitWaitTimeout { return } + if !emulateImplicitWait { + do { + try setTimeout(type: TimeoutType.implicitWait, duration: newValue) + emulateImplicitWait = true + } catch {} + } + _implicitWaitTimeout = newValue + } } + /// The amount of time interactions should be retried before failing. + /// This functionality is emulated by swift-webdriver. + public var implicitInteractionRetryTimeout: TimeInterval = .zero + /// The title of this session such as the tab or window text. public var title: String { get throws { @@ -77,8 +95,7 @@ public class Session { public var orientation: ScreenOrientation { get throws { let response = try webDriver.send(Requests.SessionOrientation.Get(session: id)) - - return response.value + return response.value } } @@ -86,6 +103,8 @@ public class Session { public func setTimeout(type: String, duration: TimeInterval) throws { try webDriver.send( Requests.SessionTimeouts(session: id, type: type, ms: duration * 1000)) + // Keep track of the implicit wait to know when we need to override it. + if type == TimeoutType.implicitWait { _implicitWaitTimeout = duration } } public func execute(script: String, args: [String] = [], async: Bool = false) throws { @@ -119,122 +138,139 @@ public class Session { /// Finds an element by id, starting from the root. /// - Parameter byId: id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter wait: Optional value to override the implicit wait timeout. /// - Returns: The element that was found, if any. - public func findElement(byId id: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(startingAt: nil, using: "id", value: id, retryTimeout: retryTimeout) + public func findElement(byId id: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(startingAt: nil, using: "id", value: id, waitTimeout: waitTimeout) } /// Finds an element by name, starting from the root. /// - Parameter byName: name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: Optional value to override defaultRetryTimeout. /// - Returns: The element that was found, if any. - public func findElement(byName name: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(startingAt: nil, using: "name", value: name, retryTimeout: retryTimeout) + public func findElement(byName name: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(startingAt: nil, using: "name", value: name, waitTimeout: waitTimeout) } /// Finds an element by accessibility id, starting from the root. /// - Parameter byAccessibilityId: accessibiilty id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: Optional value to override defaultRetryTimeout. /// - Returns: The element that was found, if any. - public func findElement(byAccessibilityId id: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(startingAt: nil, using: "accessibility id", value: id, retryTimeout: retryTimeout) + public func findElement(byAccessibilityId id: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(startingAt: nil, using: "accessibility id", value: id, waitTimeout: waitTimeout) } /// Finds an element by xpath, starting from the root. /// - Parameter byXPath: xpath of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: Optional value to override defaultRetryTimeout. /// - Returns: The element that was found, if any. - public func findElement(byXPath xpath: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(startingAt: nil, using: "xpath", value: xpath, retryTimeout: retryTimeout) + public func findElement(byXPath xpath: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(startingAt: nil, using: "xpath", value: xpath, waitTimeout: waitTimeout) } /// Finds an element by class name, starting from the root. /// - Parameter byClassName: class name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: Optional value to override defaultRetryTimeout. /// - Returns: The element that was found, if any. - public func findElement(byClassName className: String, retryTimeout: TimeInterval? = nil) throws -> Element? { - try findElement(startingAt: nil, using: "class name", value: className, retryTimeout: retryTimeout) + public func findElement(byClassName className: String, waitTimeout: TimeInterval? = nil) throws -> Element? { + try findElement(startingAt: nil, using: "class name", value: className, waitTimeout: waitTimeout) + } + + /// Overrides the implicit wait timeout during a block of code. + private func withImplicitWaitTimeout(_ value: TimeInterval?, _ block: () throws -> Result) rethrows -> Result { + if let value, value != _implicitWaitTimeout { + let previousValue = _implicitWaitTimeout + implicitWaitTimeout = value + defer { implicitWaitTimeout = previousValue } + return try block() + } + else { + return try block() + } } // Helper for findElement functions above. - internal func findElement(startingAt element: Element?, using: String, value: String, retryTimeout: TimeInterval?) throws -> Element? { + internal func findElement(startingAt element: Element?, using: String, value: String, waitTimeout: TimeInterval?) throws -> Element? { precondition(element == nil || element?.session === self) - let request = Requests.SessionElement(session: id, element: element?.id, using: using, value: value) + return try withImplicitWaitTimeout(waitTimeout) { + let request = Requests.SessionElement(session: id, element: element?.id, using: using, value: value) - let elementId = try poll(timeout: retryTimeout ?? defaultRetryTimeout) { - let elementId: String? - do { - // Allow errors to bubble up unless they are specifically saying that the element was not found. - elementId = try webDriver.send(request).value.element - } catch let error as ErrorResponse where error.status == .noSuchElement { - elementId = nil - } + let elementId = try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) { + let elementId: String? + do { + // Allow errors to bubble up unless they are specifically saying that the element was not found. + elementId = try webDriver.send(request).value.element + } catch let error as ErrorResponse where error.status == .noSuchElement { + elementId = nil + } - return PollResult(value: elementId, success: elementId != nil) - }.value + return PollResult(value: elementId, success: elementId != nil) + }.value - return elementId.map { Element(session: self, id: $0) } + return elementId.map { Element(session: self, id: $0) } + } } /// Finds elements by id, starting from the root. /// - Parameter byId: id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byId id: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(startingAt: nil, using: "id", value: id, retryTimeout: retryTimeout) + public func findElements(byId id: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(startingAt: nil, using: "id", value: id, waitTimeout: waitTimeout) } /// Finds elements by name, starting from the root. /// - Parameter byName: name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byName name: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(startingAt: nil, using: "name", value: name, retryTimeout: retryTimeout) + public func findElements(byName name: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(startingAt: nil, using: "name", value: name, waitTimeout: waitTimeout) } /// Finds elements by accessibility id, starting from the root. /// - Parameter byAccessibilityId: accessibiilty id of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byAccessibilityId id: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(startingAt: nil, using: "accessibility id", value: id, retryTimeout: retryTimeout) + public func findElements(byAccessibilityId id: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(startingAt: nil, using: "accessibility id", value: id, waitTimeout: waitTimeout) } /// Finds elements by xpath, starting from the root. /// - Parameter byXPath: xpath of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byXPath xpath: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(startingAt: nil, using: "xpath", value: xpath, retryTimeout: retryTimeout) + public func findElements(byXPath xpath: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(startingAt: nil, using: "xpath", value: xpath, waitTimeout: waitTimeout) } /// Finds elements by class name, starting from the root. /// - Parameter byClassName: class name of the element to search for. - /// - Parameter retryTimeout: Optional value to override defaultRetryTimeout. + /// - Parameter waitTimeout: Optional value to override the implicit wait timeout. /// - Returns: The elements that were found, if any. - public func findElements(byClassName className: String, retryTimeout: TimeInterval? = nil) throws -> [Element] { - try findElements(startingAt: nil, using: "class name", value: className, retryTimeout: retryTimeout) + public func findElements(byClassName className: String, waitTimeout: TimeInterval? = nil) throws -> [Element] { + try findElements(startingAt: nil, using: "class name", value: className, waitTimeout: waitTimeout) } // Helper for findElements functions above. - internal func findElements(startingAt element: Element?, using: String, value: String, retryTimeout: TimeInterval?) throws -> [Element] { - let request = Requests.SessionElements(session: id, element: element?.id, using: using, value: value) - - return try poll(timeout: retryTimeout ?? defaultRetryTimeout) { - do { - // Allow errors to bubble up unless they are specifically saying that the element was not found. - return PollResult.success(try webDriver.send(request).value.map { Element(session: self, id: $0.element) }) - } catch let error as ErrorResponse where error.status == .noSuchElement { - // Follow the WebDriver spec and keep polling if no elements are found - return PollResult.failure([]) - } - }.value + internal func findElements(startingAt element: Element?, using: String, value: String, waitTimeout: TimeInterval?) throws -> [Element] { + try withImplicitWaitTimeout(waitTimeout) { + let request = Requests.SessionElements(session: id, element: element?.id, using: using, value: value) + + return try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) { + do { + // Allow errors to bubble up unless they are specifically saying that the element was not found. + return PollResult.success(try webDriver.send(request).value.map { Element(session: self, id: $0.element) }) + } catch let error as ErrorResponse where error.status == .noSuchElement { + // Follow the WebDriver spec and keep polling if no elements are found + return PollResult.failure([]) + } + }.value + } } /// - Parameters: - /// - retryTimeout: Optional value to override defaultRetryTimeout. + /// - waitTimeout: Optional value to override defaultRetryTimeout. /// - xSpeed: The x speed in pixels per second. /// - ySpeed: The y speed in pixels per second. public func flick(xSpeed: Double, ySpeed: Double) throws { @@ -379,7 +415,7 @@ public class Session { /// 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) { + let result = try poll(timeout: retryTimeout ?? implicitInteractionRetryTimeout) { do { // Immediately bubble most failures, only retry if inconclusive. try webDriver.send(request) diff --git a/Tests/WinAppDriverTests/TimeoutTests.swift b/Tests/WinAppDriverTests/TimeoutTests.swift index 25d40a1..df986c0 100644 --- a/Tests/WinAppDriverTests/TimeoutTests.swift +++ b/Tests/WinAppDriverTests/TimeoutTests.swift @@ -37,16 +37,16 @@ class TimeoutTests: XCTestCase { let session = try startApp() // Test library timeout implementation - session.defaultRetryTimeout = 1 + session.implicitWaitTimeout = 1 XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) > 0.5) - session.defaultRetryTimeout = 0 + session.implicitWaitTimeout = 0 XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) < 0.5) } public func testWebDriverImplicitWait() throws { let session = try startApp() - session.defaultRetryTimeout = 0 + session.implicitWaitTimeout = 0 try session.setTimeout(type: TimeoutType.implicitWait, duration: 1) XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) > 0.5) From 0344624124f9926487ffd1bef6b23c1d29941523 Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Wed, 3 Jul 2024 12:00:53 -0400 Subject: [PATCH 2/3] Fix tests --- Sources/WebDriver/Session.swift | 5 +++-- Tests/WinAppDriverTests/TimeoutTests.swift | 15 +++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Sources/WebDriver/Session.swift b/Sources/WebDriver/Session.swift index b199465..b47028b 100644 --- a/Sources/WebDriver/Session.swift +++ b/Sources/WebDriver/Session.swift @@ -7,7 +7,7 @@ public class Session { public let id: String public let capabilities: Capabilities private var _implicitWaitTimeout: TimeInterval = 0 - private var emulateImplicitWait: Bool = false // Set if the session doesn't support implicit waits. + internal var emulateImplicitWait: Bool = false // Set if the session doesn't support implicit waits. private var shouldDelete: Bool = true public init(webDriver: any WebDriver, existingId: String, capabilities: Capabilities = Capabilities(), owned: Bool = false) { @@ -35,8 +35,9 @@ public class Session { if !emulateImplicitWait { do { try setTimeout(type: TimeoutType.implicitWait, duration: newValue) + } catch { emulateImplicitWait = true - } catch {} + } } _implicitWaitTimeout = newValue } diff --git a/Tests/WinAppDriverTests/TimeoutTests.swift b/Tests/WinAppDriverTests/TimeoutTests.swift index df986c0..3f6f60b 100644 --- a/Tests/WinAppDriverTests/TimeoutTests.swift +++ b/Tests/WinAppDriverTests/TimeoutTests.swift @@ -1,4 +1,5 @@ @testable import WinAppDriver +@testable import WebDriver import XCTest class TimeoutTests: XCTestCase { @@ -33,25 +34,27 @@ class TimeoutTests: XCTestCase { return Double(after.uptimeNanoseconds - before.uptimeNanoseconds) / 1_000_000_000 } - public func testLibraryImplicitWait() throws { + public func testWebDriverImplicitWait() throws { let session = try startApp() - // Test library timeout implementation session.implicitWaitTimeout = 1 XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) > 0.5) session.implicitWaitTimeout = 0 XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) < 0.5) + + XCTAssert(!session.emulateImplicitWait) } - public func testWebDriverImplicitWait() throws { + public func testEmulatedImplicitWait() throws { let session = try startApp() - session.implicitWaitTimeout = 0 - try session.setTimeout(type: TimeoutType.implicitWait, duration: 1) + // Test library timeout implementation + session.emulateImplicitWait = true + session.implicitWaitTimeout = 1 XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) > 0.5) - try session.setTimeout(type: TimeoutType.implicitWait, duration: 0) + session.implicitWaitTimeout = 0 XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) < 0.5) } } From ae38dfaea749fb36cd82659fb5908564f92415bf Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Fri, 5 Jul 2024 08:56:26 -0400 Subject: [PATCH 3/3] Updated readme --- Readme.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Readme.md b/Readme.md index a23e425..c2b5910 100644 --- a/Readme.md +++ b/Readme.md @@ -57,6 +57,12 @@ The library has two logical layers: Where WebDriver endpoint-specific functionality is provided, such as for launching and using a WinAppDriver instance, the code is kept separate from generic WebDriver functionality as much as possible. +## Timeouts +For UI testing, it is often useful to support retrying some operations until a timeout elapses (to account for animations or asynchrony). `swift-webdriver` offers two such mechanisms: + +- **Implicit wait timeout**: A duration for which `findElement` operations will implicitly wait if they cannot immediately find the element being queried. This feature is built into the WebDriver protocol, but optionally implemented by drivers. For drivers that do not support it, the library emulates it by repeating the query until the timeout elapses. By spec, this timeout defaults to zero. +- **Interaction retry timeout**: A duration for which `click`, `flick` and similar operations will retry until they result in a successful interaction (e.g. the button is not disabled). This feature is not part of the WebDriver protocol, but rather implemented by the library. This timeout defaults to zero. + ## Contributing We welcome contributions for: