Skip to content

Commit

Permalink
Introduce Session/Element.requireElement
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlabelle committed Oct 3, 2024
1 parent f578048 commit cd5255f
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 11 deletions.
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions Sources/WebDriver/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions Sources/WebDriver/ElementNotFoundError.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
2 changes: 1 addition & 1 deletion Sources/WebDriver/ErrorResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}
Expand Down
32 changes: 29 additions & 3 deletions Sources/WebDriver/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result>(_ value: TimeInterval?, _ block: () throws -> Result) rethrows -> Result {
if let value, value != _implicitWaitTimeout {
Expand All @@ -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?
Expand Down
2 changes: 1 addition & 1 deletion Tests/UnitTests/APIToRequestMappingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
8 changes: 4 additions & 4 deletions Tests/WinAppDriverTests/MSInfo32App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down

0 comments on commit cd5255f

Please sign in to comment.