diff --git a/Readme.md b/Readme.md index 1897e1d..5ef9aa9 100644 --- a/Readme.md +++ b/Readme.md @@ -11,10 +11,10 @@ A Swift library for UI automation of apps and browsers via communication with [W A `swift-webdriver` "Hello world" using `WinAppDriver` might look like this: ```swift -let session = Session( +let session = try Session( webDriver: WinAppDriver.start(), // Requires WinAppDriver to be installed on the machine desiredCapabilities: WinAppDriver.Capabilities.startApp(name: "notepad.exe")) -session.findElement(locator: .name("close"))?.click() +try session.requireElement(locator: .name("close")).click() ``` To use `swift-webdriver` in your project, add a reference to it in your `Package.swift` file as follows: diff --git a/Sources/WebDriver/Element.swift b/Sources/WebDriver/Element.swift index 05205e5..28c4cf5 100644 --- a/Sources/WebDriver/Element.swift +++ b/Sources/WebDriver/Element.swift @@ -98,6 +98,16 @@ public struct Element { try session.findElements(startingAt: self, locator: locator, waitTimeout: waitTimeout) } + /// Finds an element using a given locator, starting from this element, and throwing upon failure. + /// - Parameter locator: The locator strategy to use. + /// - Parameter description: A human-readable description of the element, included in thrown errors. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. + /// - Returns: The element that was found. + @discardableResult // for use as an assertion + public func requireElement(locator: ElementLocator, description: String? = nil, waitTimeout: TimeInterval? = nil) throws -> Element { + try session.requireElement(startingAt: self, locator: locator, description: description, waitTimeout: waitTimeout) + } + /// Gets an attribute of this element. /// - Parameter name: the attribute name. /// - Returns: the attribute value string. diff --git a/Sources/WebDriver/ElementNotFoundError.swift b/Sources/WebDriver/ElementNotFoundError.swift new file mode 100644 index 0000000..c561f5a --- /dev/null +++ b/Sources/WebDriver/ElementNotFoundError.swift @@ -0,0 +1,19 @@ +public struct ElementNotFoundError: Error { + /// The locator that was used to search for the element. + public var locator: ElementLocator + + /// A human-readable description of the element. + public var description: String? + + /// The error that caused the element to not be found. + public var sourceError: Error + + public init(locator: ElementLocator, description: String? = nil, sourceError: Error) { + self.locator = locator + self.description = description + self.sourceError = sourceError + } + + /// The error response returned by the WebDriver server, if this was the source of the failure. + public var errorResponse: ErrorResponse? { sourceError as? ErrorResponse } +} \ No newline at end of file diff --git a/Sources/WebDriver/ErrorResponse.swift b/Sources/WebDriver/ErrorResponse.swift index 8d24380..7db02c5 100644 --- a/Sources/WebDriver/ErrorResponse.swift +++ b/Sources/WebDriver/ErrorResponse.swift @@ -38,7 +38,7 @@ public struct ErrorResponse: Codable, Error { } public struct Value: Codable { - public var error: String + public var error: String? public var message: String public var stacktrace: String? } diff --git a/Sources/WebDriver/Session.swift b/Sources/WebDriver/Session.swift index 621504f..3c1852a 100644 --- a/Sources/WebDriver/Session.swift +++ b/Sources/WebDriver/Session.swift @@ -153,6 +153,32 @@ public class Session { try findElements(startingAt: nil, locator: locator, waitTimeout: waitTimeout) } + /// Finds an element using a given locator, starting from the session root, and throwing upon failure. + /// - Parameter locator: The locator strategy to use. + /// - Parameter description: A human-readable description of the element, included in thrown errors. + /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. + /// - Returns: The element that was found. + @discardableResult // for use as an assertion + public func requireElement(locator: ElementLocator, description: String? = nil, waitTimeout: TimeInterval? = nil) throws -> Element { + try requireElement(startingAt: nil, locator: locator, description: description, waitTimeout: waitTimeout) + } + + internal func requireElement(startingAt subtreeRoot: Element?, locator: ElementLocator, description: String? = nil, waitTimeout: TimeInterval? = nil) throws -> Element { + let element: Element? + do { + element = try findElement(startingAt: subtreeRoot, locator: locator, waitTimeout: waitTimeout) + } catch let error { + throw ElementNotFoundError(locator: locator, description: description, sourceError: error) + } + + guard let element else { + let synthesizedResponse = ErrorResponse(status: .noSuchElement, value: .init(message: "Element not found")) + throw ElementNotFoundError(locator: locator, description: description, sourceError: synthesizedResponse) + } + + return element + } + /// 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 { @@ -167,11 +193,11 @@ public class Session { } /// Common logic for `Session.findElement` and `Element.findElement`. - internal func findElement(startingAt element: Element?, locator: ElementLocator, waitTimeout: TimeInterval?) throws -> Element? { - precondition(element == nil || element?.session === self) + internal func findElement(startingAt subtreeRoot: Element?, locator: ElementLocator, waitTimeout: TimeInterval?) throws -> Element? { + precondition(subtreeRoot == nil || subtreeRoot?.session === self) return try withImplicitWaitTimeout(waitTimeout) { - let request = Requests.SessionElement(session: id, element: element?.id, locator: locator) + let request = Requests.SessionElement(session: id, element: subtreeRoot?.id, locator: locator) let elementId = try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) { let elementId: String? diff --git a/Tests/UnitTests/APIToRequestMappingTests.swift b/Tests/UnitTests/APIToRequestMappingTests.swift index 65d0384..3349fc2 100644 --- a/Tests/UnitTests/APIToRequestMappingTests.swift +++ b/Tests/UnitTests/APIToRequestMappingTests.swift @@ -51,7 +51,7 @@ class APIToRequestMappingTests: XCTestCase { XCTAssertEqual($0.value, "myElement.name") return ResponseWithValue(.init(element: "myElement")) } - XCTAssertNotNil(try session.findElement(locator: .name("myElement.name"))) + try session.requireElement(locator: .name("myElement.name")) mockWebDriver.expect(path: "session/mySession/element/active", method: .post, type: Requests.SessionActiveElement.self) { ResponseWithValue(.init(element: "myElement")) diff --git a/Tests/WinAppDriverTests/MSInfo32App.swift b/Tests/WinAppDriverTests/MSInfo32App.swift index 00e1a98..0a4bf5d 100644 --- a/Tests/WinAppDriverTests/MSInfo32App.swift +++ b/Tests/WinAppDriverTests/MSInfo32App.swift @@ -13,28 +13,28 @@ class MSInfo32App { } private lazy var _maximizeButton = Result { - try XCTUnwrap(session.findElement(locator: .name("Maximize")), "Maximize button not found") + try session.requireElement(locator: .name("Maximize"), description: "Maximize window button") } var maximizeButton: Element { get throws { try _maximizeButton.get() } } private lazy var _systemSummaryTree = Result { - try XCTUnwrap(session.findElement(locator: .accessibilityId("201")), "System summary tree control not found") + try session.requireElement(locator: .accessibilityId("201"), description: "System summary tree control") } var systemSummaryTree: Element { get throws { try _systemSummaryTree.get() } } private lazy var _findWhatEditBox = Result { - try XCTUnwrap(session.findElement(locator: .accessibilityId("204")), "'Find what' edit box not found") + try session.requireElement(locator: .accessibilityId("204"), description: "'Find what' edit box") } var findWhatEditBox: Element { get throws { try _findWhatEditBox.get() } } private lazy var _searchSelectedCategoryOnlyCheckbox = Result { - try XCTUnwrap(session.findElement(locator: .accessibilityId("206")), "'Search selected category only' checkbox not found") + try session.requireElement(locator: .accessibilityId("206"), description: "'Search selected category only' checkbox") } var searchSelectedCategoryOnlyCheckbox: Element { get throws { try _searchSelectedCategoryOnlyCheckbox.get() }