Skip to content

Commit

Permalink
Introduce ElementLocator to replace findElement overloads (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlabelle authored Sep 24, 2024
1 parent c321473 commit f578048
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 191 deletions.
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A `swift-webdriver` "Hello world" using `WinAppDriver` might look like this:
let session = Session(
webDriver: WinAppDriver.start(), // Requires WinAppDriver to be installed on the machine
desiredCapabilities: WinAppDriver.Capabilities.startApp(name: "notepad.exe"))
session.findElement(byName: "close")?.click()
session.findElement(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
1 change: 1 addition & 0 deletions Sources/WebDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
add_library(WebDriver
Capabilities.swift
Element.swift
ElementLocator.swift
ErrorResponse.swift
HTTPWebDriver.swift
Keys.swift
Expand Down
90 changes: 8 additions & 82 deletions Sources/WebDriver/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,94 +82,20 @@ public struct Element {
try session.sendInteraction(request, retryTimeout: retryTimeout)
}

/// Finds an element by id, starting from this element.
/// - Parameter byId: id of the element to search for.
/// Search for an element using a given locator, starting from this element.
/// - Parameter locator: The locator strategy to use.
/// - 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, waitTimeout: TimeInterval? = nil) throws -> Element? {
try findElement(using: "id", value: id, waitTimeout: waitTimeout)
public func findElement(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> Element? {
try session.findElement(startingAt: self, locator: locator, waitTimeout: waitTimeout)
}

/// Search for an element by name, starting from this element.
/// - Parameter byName: name of the element to search for.
/// - 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, 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 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, 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 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, 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 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, 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, 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.
/// Search for elements using a given locator, starting from this element.
/// - Parameter using: The locator strategy to use.
/// - 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, 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 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, 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 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, 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 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, 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 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, 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, waitTimeout: TimeInterval?) throws -> [Element] {
try session.findElements(startingAt: self, using: using, value: value, waitTimeout: waitTimeout)
public func findElements(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> [Element] {
try session.findElements(startingAt: self, locator: locator, waitTimeout: waitTimeout)
}

/// Gets an attribute of this element.
Expand Down
52 changes: 52 additions & 0 deletions Sources/WebDriver/ElementLocator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// A locator strategy to use when searching for an element.
public struct ElementLocator: Codable, Hashable {
/// The locator strategy to use.
public var using: String
/// The search target.
public var value: String

public init(using: String, value: String) {
self.using = using
self.value = value
}

/// Matches an element whose class name contains the search value; compound class names are not permitted.
public static func className(_ value: String) -> Self {
Self(using: "class name", value: value)
}

/// Matches an element matching a CSS selector.
public static func cssSelector(_ value: String) -> Self {
Self(using: "css selector", value: value)
}

/// Matches an element whose ID attribute matches the search value.
public static func id(_ value: String) -> Self {
Self(using: "id", value: value)
}

/// Matches an element whose NAME attribute matches the search value.
public static func name(_ value: String) -> Self {
Self(using: "name", value: value)
}

/// Matches an anchor element whose visible text matches the search value.
public static func linkText(_ value: String) -> Self {
Self(using: "link text", value: value)
}

/// Returns an anchor element whose visible text partially matches the search value.
public static func partialLinkText(_ value: String) -> Self {
Self(using: "partial link text", value: value)
}

/// Returns an element whose tag name matches the search value.
public static func tagName(_ value: String) -> Self {
Self(using: "tag name", value: value)
}

/// Returns an element matching an XPath expression.
public static func xpath(_ value: String) -> Self {
Self(using: "xpath", value: value)
}
}
20 changes: 4 additions & 16 deletions Sources/WebDriver/Requests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,7 @@ public enum Requests {
public struct SessionElement: Request {
public var session: String
public var element: String? = nil
public var using: String
public var value: String
public var locator: ElementLocator

public var pathComponents: [String] {
if let element {
Expand All @@ -246,12 +245,7 @@ public enum Requests {
}

public var method: HTTPMethod { .post }
public var body: Body { .init(using: using, value: value) }

public struct Body: Codable {
var using: String
var value: String
}
public var body: ElementLocator { locator }

public typealias Response = ResponseWithValue<ElementResponseValue>
}
Expand All @@ -261,8 +255,7 @@ public enum Requests {
public struct SessionElements: Request {
public var session: String
public var element: String? = nil
public var using: String
public var value: String
public var locator: ElementLocator

public var pathComponents: [String] {
if let element {
Expand All @@ -273,12 +266,7 @@ public enum Requests {
}

public var method: HTTPMethod { .post }
public var body: Body { .init(using: using, value: value) }

public struct Body: Codable {
var using: String
var value: String
}
public var body: ElementLocator { locator }

public typealias Response = ResponseWithValueArray<ElementResponseValue>
}
Expand Down
98 changes: 17 additions & 81 deletions Sources/WebDriver/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,44 +137,20 @@ public class Session {
return data
}

/// Finds an element by id, starting from the root.
/// - Parameter byId: id of the element to search for.
/// - Parameter wait: Optional value to override the implicit wait timeout.
/// - Returns: The element that was found, if any.
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 waitTimeout: Optional value to override defaultRetryTimeout.
/// - Returns: The element that was found, if any.
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 waitTimeout: Optional value to override defaultRetryTimeout.
/// - Returns: The element that was found, if any.
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 waitTimeout: Optional value to override defaultRetryTimeout.
/// Finds an element using a given locator, starting from the session root.
/// - Parameter locator: The locator strategy to use.
/// - 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(byXPath xpath: String, waitTimeout: TimeInterval? = nil) throws -> Element? {
try findElement(startingAt: nil, using: "xpath", value: xpath, waitTimeout: waitTimeout)
public func findElement(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> Element? {
try findElement(startingAt: nil, locator: locator, waitTimeout: waitTimeout)
}

/// Finds an element by class name, starting from the root.
/// - Parameter byClassName: class name of the element to search for.
/// - Parameter waitTimeout: Optional value to override defaultRetryTimeout.
/// - Returns: The element that was found, if any.
public func findElement(byClassName className: String, waitTimeout: TimeInterval? = nil) throws -> Element? {
try findElement(startingAt: nil, using: "class name", value: className, waitTimeout: waitTimeout)
/// Finds elements by id, starting from the root.
/// - Parameter locator: The locator strategy to use.
/// - 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(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> [Element] {
try findElements(startingAt: nil, locator: locator, waitTimeout: waitTimeout)
}

/// Overrides the implicit wait timeout during a block of code.
Expand All @@ -190,12 +166,12 @@ public class Session {
}
}

// Helper for findElement functions above.
internal func findElement(startingAt element: Element?, using: String, value: String, waitTimeout: TimeInterval?) throws -> Element? {
/// 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)

return try withImplicitWaitTimeout(waitTimeout) {
let request = Requests.SessionElement(session: id, element: element?.id, using: using, value: value)
let request = Requests.SessionElement(session: id, element: element?.id, locator: locator)

let elementId = try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) {
let elementId: String?
Expand All @@ -213,50 +189,10 @@ public class Session {
}
}

/// Finds elements by id, starting from the root.
/// - Parameter byId: id of the element to search for.
/// - 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, 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 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, 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 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, 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 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, 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 waitTimeout: Optional value to override the implicit wait timeout.
/// - Returns: The elements that were found, if any.
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, waitTimeout: TimeInterval?) throws -> [Element] {
/// Common logic for `Session.findElements` and `Element.findElements`.
internal func findElements(startingAt element: Element?, locator: ElementLocator, waitTimeout: TimeInterval?) throws -> [Element] {
try withImplicitWaitTimeout(waitTimeout) {
let request = Requests.SessionElements(session: id, element: element?.id, using: using, value: value)
let request = Requests.SessionElements(session: id, element: element?.id, locator: locator)

return try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) {
do {
Expand Down
1 change: 1 addition & 0 deletions Sources/WinAppDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_library(WinAppDriver
CommandLine.swift
ElementLocator+accessibilityId.swift
ErrorResponse+WinAppDriver.swift
ReexportWebDriver.swift
Win32Error.swift
Expand Down
8 changes: 8 additions & 0 deletions Sources/WinAppDriver/ElementLocator+accessibilityId.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import WebDriver

extension ElementLocator {
/// Matches an element whose accessibility ID matches the search value.
public static func accessibilityId(_ value: String) -> Self {
Self(using: "accessibility id", value: value)
}
}
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(byName: "myElement.name"))
XCTAssertNotNil(try session.findElement(locator: .name("myElement.name")))

mockWebDriver.expect(path: "session/mySession/element/active", method: .post, type: Requests.SessionActiveElement.self) {
ResponseWithValue(.init(element: "myElement"))
Expand Down
Loading

0 comments on commit f578048

Please sign in to comment.