Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Session/Element.requireElement #157

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just change findElement to return a non optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. A goal of mine is for the API to support the common scenario of negative queries without having to handle thrown errors. i.e. "don't use errors for control flow". It also has the benefits of being less noisy if you're debugging with first-chance exceptions
  2. The name findElement is neutral and I'm trying to call out that if try? findElement() == nil is a bad idea because it will wait the full timeout in the expected scenario. There might be other ways to achieve this, curious for your thoughts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

```

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 {
vinocher-bc marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be better as an extension method in our app instead? @jeffdav . I feel like it's not quite at the right layer here and I am leaning towards that (so we can have both expectElement and expectNoElement with different timeouts).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to ask, actually, because I was thinking about how to put it in the "supported apis" documentation. Could go either way. We could also have a new target in this project that is "bonus stuff".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like (nonmonetary) bonuses. I think I'll move this to the arc repo. It'll also make more sense with the custom error type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est bon.

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
Loading