From f5780485ed60d5ca968fb7091514f6c56f510ff7 Mon Sep 17 00:00:00 2001 From: Tristan Labelle Date: Tue, 24 Sep 2024 12:02:25 -0400 Subject: [PATCH] Introduce ElementLocator to replace findElement overloads (#155) --- Readme.md | 2 +- Sources/WebDriver/CMakeLists.txt | 1 + Sources/WebDriver/Element.swift | 90 ++--------------- Sources/WebDriver/ElementLocator.swift | 52 ++++++++++ Sources/WebDriver/Requests.swift | 20 +--- Sources/WebDriver/Session.swift | 98 ++++--------------- Sources/WinAppDriver/CMakeLists.txt | 1 + .../ElementLocator+accessibilityId.swift | 8 ++ .../UnitTests/APIToRequestMappingTests.swift | 2 +- Tests/WinAppDriverTests/MSInfo32App.swift | 10 +- Tests/WinAppDriverTests/RequestsTests.swift | 2 +- Tests/WinAppDriverTests/TimeoutTests.swift | 8 +- 12 files changed, 103 insertions(+), 191 deletions(-) create mode 100644 Sources/WebDriver/ElementLocator.swift create mode 100644 Sources/WinAppDriver/ElementLocator+accessibilityId.swift diff --git a/Readme.md b/Readme.md index c2b5910..1897e1d 100644 --- a/Readme.md +++ b/Readme.md @@ -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: diff --git a/Sources/WebDriver/CMakeLists.txt b/Sources/WebDriver/CMakeLists.txt index e8cd3e5..bde9e05 100644 --- a/Sources/WebDriver/CMakeLists.txt +++ b/Sources/WebDriver/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(WebDriver Capabilities.swift Element.swift + ElementLocator.swift ErrorResponse.swift HTTPWebDriver.swift Keys.swift diff --git a/Sources/WebDriver/Element.swift b/Sources/WebDriver/Element.swift index 9cc08d5..05205e5 100644 --- a/Sources/WebDriver/Element.swift +++ b/Sources/WebDriver/Element.swift @@ -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. diff --git a/Sources/WebDriver/ElementLocator.swift b/Sources/WebDriver/ElementLocator.swift new file mode 100644 index 0000000..57f1f08 --- /dev/null +++ b/Sources/WebDriver/ElementLocator.swift @@ -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) + } +} \ No newline at end of file diff --git a/Sources/WebDriver/Requests.swift b/Sources/WebDriver/Requests.swift index 9013ab4..9c88c3d 100644 --- a/Sources/WebDriver/Requests.swift +++ b/Sources/WebDriver/Requests.swift @@ -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 { @@ -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 } @@ -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 { @@ -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 } diff --git a/Sources/WebDriver/Session.swift b/Sources/WebDriver/Session.swift index 40d63d4..621504f 100644 --- a/Sources/WebDriver/Session.swift +++ b/Sources/WebDriver/Session.swift @@ -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. @@ -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? @@ -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 { diff --git a/Sources/WinAppDriver/CMakeLists.txt b/Sources/WinAppDriver/CMakeLists.txt index 27a9464..bd31fd6 100644 --- a/Sources/WinAppDriver/CMakeLists.txt +++ b/Sources/WinAppDriver/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(WinAppDriver CommandLine.swift + ElementLocator+accessibilityId.swift ErrorResponse+WinAppDriver.swift ReexportWebDriver.swift Win32Error.swift diff --git a/Sources/WinAppDriver/ElementLocator+accessibilityId.swift b/Sources/WinAppDriver/ElementLocator+accessibilityId.swift new file mode 100644 index 0000000..f6a174e --- /dev/null +++ b/Sources/WinAppDriver/ElementLocator+accessibilityId.swift @@ -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) + } +} \ No newline at end of file diff --git a/Tests/UnitTests/APIToRequestMappingTests.swift b/Tests/UnitTests/APIToRequestMappingTests.swift index 783f7f8..65d0384 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(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")) diff --git a/Tests/WinAppDriverTests/MSInfo32App.swift b/Tests/WinAppDriverTests/MSInfo32App.swift index 324767a..00e1a98 100644 --- a/Tests/WinAppDriverTests/MSInfo32App.swift +++ b/Tests/WinAppDriverTests/MSInfo32App.swift @@ -13,35 +13,35 @@ class MSInfo32App { } private lazy var _maximizeButton = Result { - try XCTUnwrap(session.findElement(byName: "Maximize"), "Maximize button not found") + try XCTUnwrap(session.findElement(locator: .name("Maximize")), "Maximize button not found") } var maximizeButton: Element { get throws { try _maximizeButton.get() } } private lazy var _systemSummaryTree = Result { - try XCTUnwrap(session.findElement(byAccessibilityId: "201"), "System summary tree control not found") + try XCTUnwrap(session.findElement(locator: .accessibilityId("201")), "System summary tree control not found") } var systemSummaryTree: Element { get throws { try _systemSummaryTree.get() } } private lazy var _findWhatEditBox = Result { - try XCTUnwrap(session.findElement(byAccessibilityId: "204"), "'Find what' edit box not found") + try XCTUnwrap(session.findElement(locator: .accessibilityId("204")), "'Find what' edit box not found") } var findWhatEditBox: Element { get throws { try _findWhatEditBox.get() } } private lazy var _searchSelectedCategoryOnlyCheckbox = Result { - try XCTUnwrap(session.findElement(byAccessibilityId: "206"), "'Search selected category only' checkbox not found") + try XCTUnwrap(session.findElement(locator: .accessibilityId("206")), "'Search selected category only' checkbox not found") } var searchSelectedCategoryOnlyCheckbox: Element { get throws { try _searchSelectedCategoryOnlyCheckbox.get() } } private lazy var _listView = Result { - let elements = try XCTUnwrap(session.findElements(byAccessibilityId: "202"), "List view not found") + let elements = try XCTUnwrap(session.findElements(locator: .accessibilityId("202")), "List view not found") try XCTSkipIf(elements.count != 1, "Expected exactly one list view; request timeout?") return elements[0] } diff --git a/Tests/WinAppDriverTests/RequestsTests.swift b/Tests/WinAppDriverTests/RequestsTests.swift index b1d0a70..2c72220 100644 --- a/Tests/WinAppDriverTests/RequestsTests.swift +++ b/Tests/WinAppDriverTests/RequestsTests.swift @@ -20,7 +20,7 @@ class RequestsTests: XCTestCase { override class func tearDown() { _app = nil } func testCanGetChildElements() throws { - let children = try XCTUnwrap(app.listView.findElements(byXPath:"//ListItem")) + let children = try XCTUnwrap(app.listView.findElements(locator: .xpath("//ListItem"))) XCTAssert(children.count > 0) } diff --git a/Tests/WinAppDriverTests/TimeoutTests.swift b/Tests/WinAppDriverTests/TimeoutTests.swift index 3f6f60b..6c32b0e 100644 --- a/Tests/WinAppDriverTests/TimeoutTests.swift +++ b/Tests/WinAppDriverTests/TimeoutTests.swift @@ -38,10 +38,10 @@ class TimeoutTests: XCTestCase { let session = try startApp() session.implicitWaitTimeout = 1 - XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) > 0.5) + XCTAssert(try Self.time({ _ = try session.findElement(locator: .accessibilityId("IdThatDoesNotExist")) }) > 0.5) session.implicitWaitTimeout = 0 - XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) < 0.5) + XCTAssert(try Self.time({ _ = try session.findElement(locator: .accessibilityId("IdThatDoesNotExist")) }) < 0.5) XCTAssert(!session.emulateImplicitWait) } @@ -52,9 +52,9 @@ class TimeoutTests: XCTestCase { // Test library timeout implementation session.emulateImplicitWait = true session.implicitWaitTimeout = 1 - XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) > 0.5) + XCTAssert(try Self.time({ _ = try session.findElement(locator: .accessibilityId("IdThatDoesNotExist")) }) > 0.5) session.implicitWaitTimeout = 0 - XCTAssert(try Self.time({ _ = try session.findElement(byAccessibilityId: "IdThatDoesNotExist") }) < 0.5) + XCTAssert(try Self.time({ _ = try session.findElement(locator: .accessibilityId("IdThatDoesNotExist")) }) < 0.5) } }