diff --git a/Example/Sources/SeleniumSwiftExample/main.swift b/Example/Sources/SeleniumSwiftExample/main.swift index 39104d8..2b82d73 100644 --- a/Example/Sources/SeleniumSwiftExample/main.swift +++ b/Example/Sources/SeleniumSwiftExample/main.swift @@ -1,14 +1,12 @@ -// main.swift +// Main.swift // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. import SwiftWebDriver internal enum Main { + /// Executable main procedure for this example package public static func main() async throws { let chromeOption = ChromeOptions( args: [ diff --git a/Package.resolved b/Package.resolved index c1eef21..84f6c96 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d4a26a5c94eaff56111cd94951b6d365f68202f4571faf25c762433afc21823c", + "originHash" : "e22cdb1ec674a3b87739341d96f88721c33a1de263371b5bc901a30274d90daa", "pins" : [ { "identity" : "async-http-client", @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "c51907a839e63ebf0ba2076bba73dd96436bd1b9", - "version" : "2.81.0" + "revision" : "663ddc80f2081c8f22e417cbac5f80270a93795e", + "version" : "2.91.0" } }, { diff --git a/Sources/SwiftWebDriver/API/APIClient.swift b/Sources/SwiftWebDriver/API/APIClient.swift index 5bcfd6f..3095cea 100644 --- a/Sources/SwiftWebDriver/API/APIClient.swift +++ b/Sources/SwiftWebDriver/API/APIClient.swift @@ -2,9 +2,6 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. import AsyncHTTPClient import Foundation @@ -25,14 +22,33 @@ internal enum APIError: Error { internal struct APIClient { private let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) + /// The shared singleton instance of `APIClient`. + /// + /// Use this when you need a shared HTTP client for sending requests to + /// the WebDriver server or other API endpoints without creating a new + /// instance each time. public static let shared = Self() - /// Request send To API and Parse Codable Models - /// - Parameter request: RequestType - /// - Returns: EventLoopFuture + /// Sends a request to the API and decodes the response into a `Codable` model. + /// + /// This method executes the given `RequestType` using the underlying + /// `AsyncHTTPClient` and decodes the result into the associated `Response` + /// type declared by the request. If the server responds with a non-OK + /// status code, the response is checked for a `SeleniumError` and thrown + /// if present; otherwise, an `APIError.responseStatsFailed` is thrown. + /// + /// - Parameter request: The request to send. Must conform to `RequestType` + /// and have a `Codable` response type. + /// - Returns: An `EventLoopFuture` that will succeed with the decoded + /// response object or fail with an error. + /// + /// - Throws: + /// - `SeleniumError` if the server returns a known WebDriver error. + /// - `APIError.responseStatsFailed` if the status code indicates failure. + /// - `APIError.responseBodyIsNil` if no response body is returned. + /// - Any `DecodingError` if the response cannot be decoded. public func request(_ request: R) -> EventLoopFuture where R: RequestType { httpClient.execute(request: request).flatMapResult { response -> Result in - guard response.status == .ok else { if let buffer = response.body, @@ -58,9 +74,20 @@ internal struct APIClient { } } - /// Request send To API and Perse Codable Models - /// - Parameter request: RequestType - /// - Returns: EventLoopFuture + /// Sends a request to the API and decodes the response into a `Codable` model, using Swift concurrency. + /// + /// This is the async/await variant of `request(_:)`, returning the decoded + /// response directly rather than wrapping it in an `EventLoopFuture`. + /// + /// - Parameter request: The request to send. Must conform to `RequestType` + /// and have a `Codable` response type. + /// - Returns: The decoded response object. + /// + /// - Throws: + /// - `SeleniumError` if the server returns a known WebDriver error. + /// - `APIError.responseStatsFailed` if the status code indicates failure. + /// - `APIError.responseBodyIsNil` if no response body is returned. + /// - Any `DecodingError` if the response cannot be decoded. @discardableResult public func request(_ request: R) async throws -> R.Response where R: RequestType { try await self.request(request).get() diff --git a/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift b/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift index dd97786..4a44171 100644 --- a/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift +++ b/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift @@ -3,50 +3,118 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -struct WebDriverElementOrigin: Encodable { - let element: String +/// Represents a complete actions payload to be sent to the WebDriver Actions API. +/// +/// Contains one or more input sources (pointer, touch, etc.) with their associated actions. +internal struct ActionsPayload: Encodable { + /// Array of input sources, each with their pointer actions. + public let actions: [PointerSource] +} - enum CodingKeys: String, CodingKey { - case element = "element-6066-11e4-a52e-4f735466cecf" +/// Represents a pointer input source (e.g., mouse) for the WebDriver Actions API. +internal struct PointerSource: Encodable { + /// Type of input source, usually `"pointer"`. + public let type: String + + /// Identifier for the input source, e.g., `"mouse"`. + public let id: String + + /// Parameters describing the input source. + public let parameters: Parameters + + /// Sequence of pointer actions for this input source. + public let actions: [PointerAction] + + /// Parameters describing the pointer input source. + internal struct Parameters: Encodable { + /// Pointer type, e.g., `"mouse"`, `"pen"`, `"touch"`. + public let pointerType: String } } -struct PointerAction: Encodable { - let type: String - let origin: WebDriverElementOrigin? - let x: Int? - let y: Int? - let button: Int? - let duration: Int? +/// Represents a single pointer action for use in the WebDriver Actions API. +/// +/// Examples of actions include pointer move, pointer down/up, and pauses. +internal struct PointerAction: Encodable { + /// The type of the action, e.g., `"pointerMove"`, `"pointerDown"`, `"pointerUp"`, `"pause"`. + public let type: String + + /// Optional origin element for the action. + public let origin: WebDriverElementOrigin? + + /// Optional x-coordinate for the pointer action. + public let xCoordinate: Int? + + /// Optional y-coordinate for the pointer action. + public let yCoordinate: Int? + + /// Optional button for pointer actions (0 = left, 1 = middle, 2 = right). + public let button: Int? + + /// Optional duration in milliseconds for pause actions. + public let duration: Int? - init( + /// Coding keys for encoding/decoding `PointerAction` to/from JSON. + /// + /// Maps the Swift property names to the corresponding keys expected by the WebDriver Actions API. + public enum CodingKeys: String, CodingKey { + /// Action type (e.g., "pointerMove", "pointerDown", "pointerUp"). + case type + + /// Origin element for the pointer action. + case origin + + /// X-coordinate of the pointer action. + case xCoordinate = "x" + + /// Y-coordinate of the pointer action. + case yCoordinate = "y" + + /// Mouse button involved in the action (0 = left, 1 = middle, 2 = right). + case button + + /// Duration of the action in milliseconds. + case duration + } + + /// Initializes a new `PointerAction`. + /// + /// - Parameters: + /// - type: The type of pointer action. + /// - origin: The element origin for the action (optional). + /// - xCoordinate: X-coordinate (optional). + /// - yCoordinate: Y-coordinate (optional). + /// - button: Button index for mouse actions (optional). + /// - duration: Duration in milliseconds for pause actions (optional). + public init( type: String, origin: WebDriverElementOrigin? = nil, - x: Int? = nil, - y: Int? = nil, + xCoordinate: Int? = nil, + yCoordinate: Int? = nil, button: Int? = nil, duration: Int? = nil ) { self.type = type self.origin = origin - self.x = x - self.y = y + self.xCoordinate = xCoordinate + self.yCoordinate = yCoordinate self.button = button self.duration = duration } } -struct PointerSource: Encodable { - let type: String - let id: String - let parameters: Parameters - let actions: [PointerAction] +/// Represents a reference to a WebDriver element in the Actions API. +/// +/// This is used as the origin for pointer or touch actions. +internal struct WebDriverElementOrigin: Encodable { + /// The element ID in WebDriver protocol format. + public let element: String - struct Parameters: Encodable { - let pointerType: String + /// Coding keys for encoding/decoding `WebDriverElementOrigin` to/from JSON. + /// + /// Maps the Swift property `element` to the WebDriver protocol’s expected key. + public enum CodingKeys: String, CodingKey { + /// The WebDriver element identifier key used in actions payloads. + case element = "element-6066-11e4-a52e-4f735466cecf" } } - -struct ActionsPayload: Encodable { - let actions: [PointerSource] -} diff --git a/Sources/SwiftWebDriver/API/Request/DevTools/AnyEncodable.swift b/Sources/SwiftWebDriver/API/Request/DevTools/AnyEncodable.swift index b705110..076ec3e 100644 --- a/Sources/SwiftWebDriver/API/Request/DevTools/AnyEncodable.swift +++ b/Sources/SwiftWebDriver/API/Request/DevTools/AnyEncodable.swift @@ -3,13 +3,33 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +/// A type-erased wrapper for any `Encodable` value. +/// +/// `AnyEncodable` allows you to store or pass around values of different `Encodable` +/// types without knowing their concrete type at compile time. This is useful when +/// building heterogeneous collections of `Encodable` objects or creating dynamic +/// JSON payloads. public struct AnyEncodable: Encodable { + // MARK: - Private Properties + + /// The internal closure used to encode the wrapped value. private let encodeFunc: (Encoder) throws -> Void - init(_ value: some Encodable) { + // MARK: - Initializers + + /// Creates a type-erased wrapper around the given `Encodable` value. + /// + /// - Parameter value: Any value that conforms to `Encodable`. + public init(_ value: some Encodable) { encodeFunc = value.encode } + // MARK: - Encoding + + /// Encodes the wrapped value using the given encoder. + /// + /// - Parameter encoder: The encoder to write data to. + /// - Throws: Any encoding error thrown by the wrapped value. public func encode(to encoder: Encoder) throws { try encodeFunc(encoder) } diff --git a/Sources/SwiftWebDriver/API/Request/DevTools/DevToolTypes.swift b/Sources/SwiftWebDriver/API/Request/DevTools/DevToolTypes.swift index 2c2d50a..9a85032 100644 --- a/Sources/SwiftWebDriver/API/Request/DevTools/DevToolTypes.swift +++ b/Sources/SwiftWebDriver/API/Request/DevTools/DevToolTypes.swift @@ -2,12 +2,26 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. +/// A namespace containing types related to browser developer tool operations. +/// +/// `DevToolTypes` groups together constants and enumerations that define +/// configuration or execution modes for actions performed via browser +/// developer tools. public enum DevToolTypes { + /// Represents the available modes for executing JavaScript through + /// browser developer tools. public enum JavascriptExecutionTypes { - case async, sync + /// Executes JavaScript **asynchronously**. + /// + /// The execution returns a result only after an asynchronous operation + /// completes, allowing for `Promise` handling or delayed callbacks. + case async + + /// Executes JavaScript **synchronously**. + /// + /// The execution blocks until a result is immediately available, + /// without waiting for asynchronous operations. + case sync } } diff --git a/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteAsyncRequest.swift b/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteAsyncRequest.swift deleted file mode 100644 index d0d8fe8..0000000 --- a/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteAsyncRequest.swift +++ /dev/null @@ -1,47 +0,0 @@ -// PostExecuteAsyncRequest.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. - -import AsyncHTTPClient -import Foundation -import NIO -import NIOHTTP1 - -internal struct PostExecuteAsyncRequest: RequestType { - public typealias Response = PostExecuteResponse - - public var baseURL: URL - - public var sessionId: String - - public var path: String { - "session/\(sessionId)/execute/async" - } - - public let javascriptSnippet: RequestBody - - public var method: HTTPMethod = .POST - - public var headers: HTTPHeaders = [:] - - public var body: HTTPClient.Body? { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - let data = try? encoder.encode(javascriptSnippet) - - guard let data else { - return nil - } - - return .data(data) - } - - struct RequestBody: Encodable { - let script: String - let args: [AnyEncodable] - } -} diff --git a/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteRequest.swift b/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteRequest.swift new file mode 100644 index 0000000..5ea476c --- /dev/null +++ b/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteRequest.swift @@ -0,0 +1,90 @@ +// PostExecuteRequest.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import AsyncHTTPClient +import Foundation +import NIO +import NIOHTTP1 + +/// A WebDriver request for executing JavaScript in the context of the browser. +/// +/// `PostExecuteRequest` can execute JavaScript either synchronously or asynchronously +/// depending on the `type` parameter. The request sends the script and optional arguments +/// to the WebDriver session and returns a `PostExecuteResponse` containing the result. +/// +/// Conforms to `RequestType` for use with `APIClient`. +internal struct PostExecuteRequest: RequestType { + // MARK: - Associated Types + + /// The expected response type returned by executing the JavaScript snippet. + public typealias Response = PostExecuteResponse + + // MARK: - Properties + + /// The base URL of the WebDriver server. + public var baseURL: URL + + /// The active WebDriver session identifier. + public var sessionId: String + + /// Determines whether the JavaScript is executed synchronously or asynchronously. + public var type: DevToolTypes.JavascriptExecutionTypes + + /// The request path relative to `baseURL`. + /// + /// - If `type` is `.sync`, the request uses `session/{sessionId}/execute/sync`. + /// - Otherwise, it uses `session/{sessionId}/execute/async`. + public var path: String { + switch type { + case .sync: + "session/\(sessionId)/execute/sync" + default: + "session/\(sessionId)/execute/async" + } + } + + /// The JavaScript snippet and its arguments to execute. + public let javascriptSnippet: RequestBody + + /// The HTTP method used for this request. + /// + /// Always `.POST` because WebDriver requires POST for JavaScript execution. + public var method: HTTPMethod = .POST + + /// The HTTP headers for this request. + /// + /// Defaults to an empty header set. + public var headers: HTTPHeaders = [:] + + /// The HTTP request body containing the encoded JavaScript snippet and arguments. + /// + /// Returns `nil` if encoding fails. + public var body: HTTPClient.Body? { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + guard let data = try? encoder.encode(javascriptSnippet) else { + return nil + } + + return .data(data) + } +} + +// MARK: - Nested Types + +internal extension PostExecuteRequest { + /// The body of the JavaScript execution request. + /// + /// Contains the script string and an array of arguments, which are type-erased + /// using `AnyEncodable` to allow any `Encodable` value to be passed to the script. + struct RequestBody: Encodable { + /// The JavaScript code to execute. + public let script: String + + /// The arguments to pass to the script. + public let args: [AnyEncodable] + } +} diff --git a/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteSyncRequest.swift b/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteSyncRequest.swift deleted file mode 100644 index acb2667..0000000 --- a/Sources/SwiftWebDriver/API/Request/DevTools/PostExecuteSyncRequest.swift +++ /dev/null @@ -1,48 +0,0 @@ -// PostExecuteSyncRequest.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. - -import AsyncHTTPClient -import Foundation -import NIO -import NIOHTTP1 - -internal struct PostExecuteSyncRequest: RequestType { - public typealias Response = PostExecuteResponse - - public var baseURL: URL - - public var sessionId: String - - public var path: String { - "session/\(sessionId)/execute/sync" - } - - public let javascriptSnippet: RequestBody - - public var method: HTTPMethod = .POST - - public var headers: HTTPHeaders = [:] - - public var body: HTTPClient.Body? { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - - guard let data = try? encoder.encode(javascriptSnippet) else { - return nil - } - - return .data(data) - } -} - -internal extension PostExecuteSyncRequest { - struct RequestBody: Encodable { - let script: String - let args: [AnyEncodable] - } -} diff --git a/Sources/SwiftWebDriver/API/Request/Elements/ElementsTypes.swift b/Sources/SwiftWebDriver/API/Request/Elements/ElementsTypes.swift index 03f1b66..35bdb28 100644 --- a/Sources/SwiftWebDriver/API/Request/Elements/ElementsTypes.swift +++ b/Sources/SwiftWebDriver/API/Request/Elements/ElementsTypes.swift @@ -2,16 +2,38 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. +/// A namespace containing type definitions related to HTML elements and input interactions. +/// +/// `ElementsTypes` serves as a grouping for various subtypes used when interacting +/// with elements, such as keyboard input key codes. public enum ElementsTypes { + /// Represents special key codes that can be sent to an element when performing a `sendKeys`-type action. + /// + /// These key codes use WebDriver's Unicode-based representation for non-text keys + /// such as `Enter`, `Return`, and `Tab`. They can be sent along with regular + /// text input to simulate keyboard events in browser automation. public enum SendValueActionKeyTypes: String { + /// The **Enter** key (Unicode: `\u{E007}`). + /// + /// Typically used to submit forms or trigger button actions in web pages. case ENTER1 = "\u{E007}" + + /// The **Return** key (Unicode: `\u{E006}`). + /// + /// Often treated similarly to `Enter`, but provided as a separate code + /// for systems that distinguish between them. case RETURN1 = "\u{E006}" + + /// The **Tab** key (Unicode: `\u{E004}`). + /// + /// Used to move focus between interactive elements (e.g., form fields) in a web page. case TAB = "\u{E004}" + /// The raw Unicode representation of the key. + /// + /// This string value is what will be sent to the WebDriver `sendKeys` + /// command when simulating a key press. public var unicode: String { rawValue } diff --git a/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift b/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift index 53553cb..9a4073b 100644 --- a/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift +++ b/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift @@ -8,15 +8,31 @@ import Foundation import NIO import NIOHTTP1 +/// Request to retrieve the rectangle (position and size) of a DOM element using the WebDriver API. +/// +/// Conforms to `RequestType` to be used with `APIClient`. internal struct GetElementRectRequest: RequestType { - typealias Response = GetElementRectResponse + /// Response type returned by this request. + public typealias Response = GetElementRectResponse - var baseURL: URL - var sessionId: String - var elementId: String + /// Base URL of the WebDriver server. + public var baseURL: URL - var path: String { "session/\(sessionId)/element/\(elementId)/rect" } - var method: HTTPMethod = .GET - var headers: HTTPHeaders = [:] - var body: HTTPClient.Body? { nil } + /// Session ID of the active WebDriver session. + public var sessionId: String + + /// WebDriver element ID for which the rectangle is requested. + public var elementId: String + + /// Endpoint path for retrieving the element rectangle. + public var path: String { "session/\(sessionId)/element/\(elementId)/rect" } + + /// HTTP method used for this request (`GET`). + public var method: HTTPMethod = .GET + + /// HTTP headers for the request (empty by default). + public var headers: HTTPHeaders = [:] + + /// HTTP request body (none required for this request). + public var body: HTTPClient.Body? { nil } } diff --git a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift index c8f92f0..c789f18 100644 --- a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift +++ b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift @@ -9,28 +9,74 @@ import Foundation import NIO import NIOHTTP1 -internal struct PostElementDoubleClickRequest: RequestType { - typealias Response = PostElementClickResponse +/// A WebDriver request that performs a **double-click** action on a specific element. +/// +/// This request sends a sequence of pointer actions to the remote WebDriver session +/// to simulate two consecutive mouse clicks on the same element, effectively triggering +/// a double-click event in the browser. +/// +/// Conforms to `RequestType` so it can be executed by the API client and return a +/// `PostElementClickResponse` upon completion. +public struct PostElementDoubleClickRequest: RequestType { + // MARK: - Associated Types - var baseURL: URL + /// The expected response type for this request. + /// + /// A `PostElementClickResponse` is returned by the WebDriver server + /// after the double-click action has been successfully performed. + public typealias Response = PostElementClickResponse - var sessionId: String + // MARK: - Properties - var elementId: String + /// The base URL of the WebDriver server. + /// + /// Typically points to the running browser automation instance + /// (e.g., `http://localhost:9515` for ChromeDriver). + public var baseURL: URL - var path: String { + /// The active WebDriver session identifier. + /// + /// This is used to scope the request to a specific browser session. + public var sessionId: String + + /// The identifier of the target element to double-click. + /// + /// This ID must have been obtained from a previous element lookup request. + public var elementId: String + + /// The request path relative to `baseURL`. + /// + /// Combines the session ID with the `/actions` endpoint, which is used for + /// sending complex user input commands to WebDriver. + public var path: String { "session/\(sessionId)/actions" } - var method: HTTPMethod = .POST + /// The HTTP method for this request. + /// + /// Always `.POST` for WebDriver action sequences. + public var method: HTTPMethod = .POST - var headers: HTTPHeaders = [:] + /// The HTTP headers for this request. + /// + /// Defaults to an empty header set, allowing `Content-Type` and others to be + /// applied automatically by the HTTP client. + public var headers: HTTPHeaders = [:] - var body: HTTPClient.Body? { + /// The HTTP request body containing the encoded pointer action sequence. + /// + /// The body includes: + /// - A pointer move to the element's origin + /// - A click (pointer down + pointer up) + /// - A short pause + /// - Another click (pointer down + pointer up) + /// + /// This sequence simulates a user performing a double-click with a mouse. + public var body: HTTPClient.Body? { let origin = WebDriverElementOrigin(element: elementId) let pointerActions = [ - PointerAction(type: "pointerMove", origin: origin, x: 0, y: 0), + PointerAction(type: "pointerMove", origin: origin, xCoordinate: 0, yCoordinate: 0), PointerAction(type: "pointerDown", button: 0), PointerAction(type: "pointerUp", button: 0), PointerAction(type: "pause", duration: 50), diff --git a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift index 8116f1c..cb2ec9c 100644 --- a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift +++ b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift @@ -9,61 +9,137 @@ import Foundation import NIO import NIOHTTP1 +/// A WebDriver request that performs a drag-and-drop operation from one element to another. +/// +/// `PostElementDragAndDropRequest` generates the actions payload required by Selenium/WebDriver +/// to simulate dragging a source element to a target element using pointer events. +/// +/// Conforms to `RequestType` for use with `APIClient`. internal struct PostElementDragAndDropRequest: RequestType { - typealias Response = PostElementClickResponse + // MARK: - Associated Types - var baseURL: URL + /// The expected response type returned by the drag-and-drop request. + public typealias Response = PostElementClickResponse - var sessionId: String + // MARK: - Properties - var elementId: String + /// The base URL of the WebDriver server. + public var baseURL: URL - var toElementId: String + /// The active WebDriver session identifier. + public var sessionId: String - var elementRect: ElementRect + /// The element ID of the source element to drag. + public var elementId: String - var targetElementRect: ElementRect + /// The element ID of the target element to drop onto. + public var toElementId: String - var path: String { + /// The rectangle representing the size and position of the target element. + public var targetElementRect: ElementRect + + /// The request path relative to `baseURL`. + /// + /// Always `"session/{sessionId}/actions"` for performing drag-and-drop pointer actions. + public var path: String { "session/\(sessionId)/actions" } - var method: HTTPMethod = .POST + /// The HTTP method used for this request. + /// + /// Always `.POST` because WebDriver requires POST for actions. + public var method: HTTPMethod = .POST + + /// The HTTP headers for this request. + /// + /// Defaults to an empty header set. + public var headers: HTTPHeaders = [:] + + /// The HTTP request body containing the encoded pointer actions payload. + /// + /// Returns `nil` if encoding the payload fails. + public var body: HTTPClient.Body? { + getEncodedActionsPayload() + } - var headers: HTTPHeaders = [:] + // MARK: - Private Helpers - var body: HTTPClient.Body? { - let origin = WebDriverElementOrigin(element: elementId) - let dragToOrigin = WebDriverElementOrigin(element: toElementId) + /// Encodes the actions payload into `HTTPClient.Body`. + /// + /// - Returns: Encoded body data or `nil` if encoding fails. + private func getEncodedActionsPayload() -> HTTPClient.Body? { + let payload = getActionsPayload() + guard let data = encodeActionsPayload(payload: payload) else { + return nil + } + return .data(data) + } - let targetCenterX = Int(targetElementRect.width / 2) - let targetCenterY = Int(targetElementRect.height / 2) + /// Encodes an `ActionsPayload` object to JSON data. + /// + /// - Parameter payload: The payload to encode. + /// - Returns: Encoded data or `nil` if encoding fails. + private func encodeActionsPayload(payload: ActionsPayload) -> Data? { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + return try? encoder.encode(payload) + } - let pointerActions = [ - PointerAction(type: "pointerMove", origin: origin, x: 0, y: 0), - PointerAction(type: "pointerDown", button: 0), - PointerAction(type: "pause", duration: 100), - PointerAction(type: "pointerMove", origin: dragToOrigin, x: targetCenterX, y: targetCenterY), - PointerAction(type: "pointerUp", button: 0) - ] + /// Constructs the full `ActionsPayload` for the drag-and-drop operation. + /// + /// - Returns: An `ActionsPayload` containing pointer actions. + private func getActionsPayload() -> ActionsPayload { + ActionsPayload(actions: [getPointerSource()]) + } - let pointerSource = PointerSource( + /// Creates a `PointerSource` object representing the mouse pointer for the drag-and-drop. + /// + /// - Returns: A `PointerSource` with drag-and-drop actions. + private func getPointerSource() -> PointerSource { + PointerSource( type: "pointer", id: "mouse", parameters: .init(pointerType: "mouse"), - actions: pointerActions + actions: getDragAndDropPointerActions() ) + } - let payload = ActionsPayload(actions: [pointerSource]) + /// Generates the sequence of `PointerAction` objects to perform drag-and-drop. + /// + /// - Returns: An array of pointer actions simulating a drag from the source to the target. + private func getDragAndDropPointerActions() -> [PointerAction] { + let (origin, dragToOrigin) = getElementOrigins() + let (targetCenterX, targetCenterY) = getElementCenterCoordinates() - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - let data = try? encoder.encode(payload) + return [ + PointerAction(type: "pointerMove", origin: origin, xCoordinate: 0, yCoordinate: 0), + PointerAction(type: "pointerDown", button: 0), + PointerAction(type: "pause", duration: 100), + PointerAction( + type: "pointerMove", + origin: dragToOrigin, + xCoordinate: targetCenterX, + yCoordinate: targetCenterY + ), + PointerAction(type: "pointerUp", button: 0) + ] + } - guard let data else { - return nil - } + /// Calculates the center coordinates of the target element for accurate drop location. + /// + /// - Returns: A tuple `(x, y)` representing the center of the target element. + private func getElementCenterCoordinates() -> (Int, Int) { + let targetCenterX = Int(targetElementRect.width / 2) + let targetCenterY = Int(targetElementRect.height / 2) + return (targetCenterX, targetCenterY) + } - return .data(data) + /// Returns the WebDriver origins for the source and target elements. + /// + /// - Returns: A tuple `(sourceOrigin, targetOrigin)` representing element references. + private func getElementOrigins() -> (WebDriverElementOrigin, WebDriverElementOrigin) { + let origin = WebDriverElementOrigin(element: elementId) + let dragToOrigin = WebDriverElementOrigin(element: toElementId) + return (origin, dragToOrigin) } } diff --git a/Sources/SwiftWebDriver/API/Request/RequestType.swift b/Sources/SwiftWebDriver/API/Request/RequestType.swift index 36832fb..3659e88 100644 --- a/Sources/SwiftWebDriver/API/Request/RequestType.swift +++ b/Sources/SwiftWebDriver/API/Request/RequestType.swift @@ -2,9 +2,6 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. import AsyncHTTPClient import Foundation diff --git a/Sources/SwiftWebDriver/Element/Element.swift b/Sources/SwiftWebDriver/Element/Element.swift index a6fcb0c..3c6b14b 100644 --- a/Sources/SwiftWebDriver/Element/Element.swift +++ b/Sources/SwiftWebDriver/Element/Element.swift @@ -6,18 +6,55 @@ import Foundation import NIO +/// A protocol defining methods for locating DOM elements within a web page. +/// +/// Types that conform to `FindElementProtocol` (e.g., `Driver` or `Element`) provide +/// functionality to find a single element, multiple elements, or wait until an element +/// exists in the DOM. public protocol FindElementProtocol { + // MARK: - Element Lookup + + /// Finds a single DOM element matching the specified locator type. + /// + /// - Parameter locatorType: The strategy used to locate the element + /// (e.g., CSS selector, XPath, ID). + /// - Returns: An `Element` representing the found DOM element. + /// - Throws: An error if the element cannot be found or if the WebDriver + /// request fails. func findElement(_ locatorType: LocatorType) async throws -> Element + + /// Finds all DOM elements matching the specified locator type. + /// + /// - Parameter locatorType: The strategy used to locate the elements + /// (e.g., CSS selector, XPath, class name). + /// - Returns: An array of `Element` objects representing the found elements. + /// - Throws: An error if the WebDriver request fails. func findElements(_ locatorType: LocatorType) async throws -> Elements - func waitUntil(_ locatorType: LocatorType, retryCount: Int, durationSeconds: Int) async throws -> Bool + + // MARK: - Waiting + + /// Waits until at least one element matching the locator type exists in the DOM, + /// retrying for a specified number of attempts with a delay between each retry. + /// + /// - Parameters: + /// - locatorType: The strategy used to locate the element. + /// - retryCount: The number of retry attempts (default is 3). + /// - durationSeconds: The delay in seconds between retries (default is 1). + /// - Returns: `true` if the element was found within the retry attempts; otherwise `false`. + /// - Throws: Any errors thrown during element lookup that are not handled as "no such element". + func waitUntil( + _ locatorType: LocatorType, + retryCount: Int, + durationSeconds: Int + ) async throws -> Bool } -public protocol ElementCommandProtocol: FindElementProtocol { +internal protocol ElementCommandProtocol: FindElementProtocol { func text() async throws -> String func name() async throws -> String func click() async throws -> String? func doubleClick() async throws -> String? - func dragAndDrop(to: Element) async throws -> String? + func dragAndDrop(to target: Element) async throws -> String? func clear() async throws -> String? func attribute(name: String) async throws -> String func send(value: String) async throws -> String? @@ -25,11 +62,31 @@ public protocol ElementCommandProtocol: FindElementProtocol { func rect() async throws -> ElementRect } +/// Represents a DOM element within a WebDriver session. +/// +/// The `Element` struct provides methods to locate child elements, interact with the element, +/// and retrieve its attributes or properties using the Selenium WebDriver protocol. +/// +/// Instances of `Element` are created by locating elements through a driver or another element. +/// Each `Element` instance is tied to a specific WebDriver session and element ID. +/// +/// - Note: All methods are asynchronous and throw on failure. +/// - Warning: The underlying WebDriver session must remain valid for the lifetime of this element. public struct Element: ElementCommandProtocol, Sendable { + /// The base URL of the WebDriver instance. public let baseURL: URL + + /// The session identifier of the active WebDriver session. public let sessionId: String + + /// The unique element identifier assigned by WebDriver. public let elementId: String + /// Finds the first child element matching the given locator. + /// + /// - Parameter locatorType: The locator strategy and value (e.g., CSS selector, XPath). + /// - Returns: A new `Element` representing the located child element. + /// - Throws: An error if no matching element is found or if the request fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func findElement(_ locatorType: LocatorType) async throws -> Self { @@ -43,6 +100,11 @@ public struct Element: ElementCommandProtocol, Sendable { return Self(baseURL: baseURL, sessionId: sessionId, elementId: response.elementId) } + /// Finds all child elements matching the given locator. + /// + /// - Parameter locatorType: The locator strategy and value. + /// - Returns: An array of `Element` instances for all matched elements. + /// - Throws: An error if the request fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func findElements(_ locatorType: LocatorType) async throws -> Elements { let request = PostElementsByIdRequest( @@ -57,6 +119,10 @@ public struct Element: ElementCommandProtocol, Sendable { } } + /// Retrieves the visible text content of the element. + /// + /// - Returns: The text contained within the element. + /// - Throws: An error if retrieval fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func text() async throws -> String { let request = GetElementTextRequest(baseURL: baseURL, sessionId: sessionId, elementId: elementId) @@ -64,6 +130,10 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Retrieves the element's tag name (e.g., `"div"`, `"input"`). + /// + /// - Returns: The tag name of the element. + /// - Throws: An error if retrieval fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func name() async throws -> String { let request = GetElementNameRequest(baseURL: baseURL, sessionId: sessionId, elementId: elementId) @@ -71,6 +141,10 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Clicks the element. + /// + /// - Returns: A response string if provided by the WebDriver, otherwise `nil`. + /// - Throws: An error if the click action fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func click() async throws -> String? { @@ -79,6 +153,10 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Performs a double-click action on the element. + /// + /// - Returns: A response string if provided by the WebDriver, otherwise `nil`. + /// - Throws: An error if the double-click action fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func doubleClick() async throws -> String? { @@ -87,21 +165,29 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Drags this element and drops it onto another target element. + /// + /// - Parameter target: The element to drop this element onto. + /// - Returns: A response string if provided by the WebDriver, otherwise `nil`. + /// - Throws: An error if the drag-and-drop operation fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public func dragAndDrop(to: Element) async throws -> String? { + public func dragAndDrop(to target: Self) async throws -> String? { let request = try await PostElementDragAndDropRequest( baseURL: baseURL, sessionId: sessionId, elementId: elementId, - toElementId: to.elementId, - elementRect: rect(), - targetElementRect: to.rect() + toElementId: target.elementId, + targetElementRect: target.rect() ) let response = try await APIClient.shared.request(request) return response.value } + /// Retrieves the position and size of the element. + /// + /// - Returns: An `ElementRect` containing the element's coordinates and dimensions. + /// - Throws: An error if retrieval fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func rect() async throws -> ElementRect { let request = GetElementRectRequest( @@ -113,6 +199,10 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Clears the text from an input or textarea element. + /// + /// - Returns: A response string if provided by the WebDriver, otherwise `nil`. + /// - Throws: An error if the clear operation fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func clear() async throws -> String? { @@ -121,6 +211,11 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Retrieves the value of the specified attribute for the element. + /// + /// - Parameter name: The name of the attribute to retrieve. + /// - Returns: The attribute value as a string. + /// - Throws: An error if retrieval fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func attribute(name: String) async throws -> String { let request = GetElementAttributeRequest( @@ -133,6 +228,11 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Sends text input to the element (e.g., typing into a text field). + /// + /// - Parameter value: The string to input. + /// - Returns: A response string if provided by the WebDriver, otherwise `nil`. + /// - Throws: An error if sending the value fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func send(value: String) async throws -> String? { @@ -146,6 +246,11 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Sends a special key or key combination to the element. + /// + /// - Parameter value: A predefined key type to send. + /// - Returns: A response string if provided by the WebDriver, otherwise `nil`. + /// - Throws: An error if sending the key fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func send(value: ElementsTypes.SendValueActionKeyTypes) async throws -> String? { @@ -159,6 +264,10 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Takes a screenshot of the element. + /// + /// - Returns: A Base64-encoded string representing the image. + /// - Throws: An error if the screenshot operation fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func screenshot() async throws -> String { let request = GetElementScreenShotRequest(baseURL: baseURL, sessionId: sessionId, elementId: elementId) @@ -166,6 +275,14 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + /// Waits until an element matching the given locator appears within this element's DOM subtree. + /// + /// - Parameters: + /// - locatorType: The locator strategy and value. + /// - retryCount: Number of retry attempts before giving up. Defaults to `3`. + /// - durationSeconds: Delay between retries in seconds. Defaults to `1`. + /// - Returns: `true` if the element is found, otherwise `false`. + /// - Throws: Any non-"no such element" error from WebDriver. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func waitUntil( diff --git a/Sources/SwiftWebDriver/Element/ElementRect.swift b/Sources/SwiftWebDriver/Element/ElementRect.swift index f5fd9da..417caf3 100644 --- a/Sources/SwiftWebDriver/Element/ElementRect.swift +++ b/Sources/SwiftWebDriver/Element/ElementRect.swift @@ -3,9 +3,35 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +/// Represents the position and size of a DOM element. +/// +/// The `ElementRect` struct contains the element's coordinates and dimensions +/// as reported by the WebDriver API. This is typically used for layout measurements, +/// drag-and-drop calculations, and other geometry-based interactions. +/// +/// - Note: Coordinates are expressed in CSS pixels relative to the top-left corner +/// of the document's viewport. public struct ElementRect: Codable, Sendable { - public let x: Double - public let y: Double + /// The horizontal position of the element's top-left corner, in CSS pixels. + public let xPosition: Double + + /// The vertical position of the element's top-left corner, in CSS pixels. + public let yPosition: Double + + /// The width of the element, in CSS pixels. public let width: Double + + /// The height of the element, in CSS pixels. public let height: Double + + /// Coding keys for mapping WebDriver's JSON response to Swift property names. + /// + /// WebDriver typically returns `"x"` and `"y"` for coordinates, + /// which are mapped here to `xPosition` and `yPosition` respectively. + public enum CodingKeys: String, CodingKey { + case xPosition = "x" + case yPosition = "y" + case width + case height + } } diff --git a/Sources/SwiftWebDriver/Element/Elements.swift b/Sources/SwiftWebDriver/Element/Elements.swift index 494e2c0..117d521 100644 --- a/Sources/SwiftWebDriver/Element/Elements.swift +++ b/Sources/SwiftWebDriver/Element/Elements.swift @@ -2,17 +2,30 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. import Foundation +/// A type alias for an array of `Element` instances. +/// +/// `Elements` is used to represent multiple DOM elements returned by queries +/// such as `findElements` or batch operations. public typealias Elements = [Element] +/// Extension adding convenience methods for working with multiple `Element` instances. +/// +/// These methods are designed for batch operations, allowing asynchronous actions +/// (like retrieving text or attributes) to be performed concurrently on each element. public extension Elements { + /// Retrieves the visible text content of each element in the collection. + /// + /// This method performs requests concurrently for all elements and returns + /// their `innerText` values in the same order they complete (not necessarily + /// the order they appear in the array). + /// + /// - Returns: An array of strings, each representing an element's visible text. + /// - Throws: An error if any of the underlying WebDriver requests fail. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - private func texts() async throws -> [String] { + func texts() async throws -> [String] { var ids: [String] = [] try await withThrowingTaskGroup(of: GetElementTextResponse.self) { group in for element in self { @@ -34,8 +47,14 @@ public extension Elements { return ids } + /// Retrieves the tag name of each element in the collection. + /// + /// This method performs requests concurrently for all elements. + /// + /// - Returns: An array of strings containing each element's tag name. + /// - Throws: An error if any of the underlying WebDriver requests fail. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - private func names() async throws -> [String] { + func names() async throws -> [String] { var names: [String] = [] try await withThrowingTaskGroup(of: GetElementNameResponse.self) { group in for element in self { @@ -57,12 +76,18 @@ public extension Elements { return names } + /// Finds the first matching descendant element within each element in the collection. + /// + /// This is equivalent to calling `findElement(_:)` on each element, and it runs all lookups concurrently. + /// + /// - Parameter locatorType: The method by which to locate the element (e.g., `.css`, `.xpath`). + /// - Returns: An array of found `Element` instances. + /// - Throws: An error if no matching element is found or if a WebDriver request fails. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - private func findElement(_ locatorType: LocatorType) async throws -> Elements { + func findElement(_ locatorType: LocatorType) async throws -> Elements { var elements: Elements = [] try await withThrowingTaskGroup(of: Element.self) { group in - for element in self { group.addTask { try await element.findElement(locatorType) @@ -77,8 +102,15 @@ public extension Elements { return elements } + /// Finds all matching descendant elements within each element in the collection. + /// + /// This is equivalent to calling `findElements(_:)` on each element, and it runs all lookups concurrently. + /// + /// - Parameter locatorType: The method by which to locate the elements (e.g., `.css`, `.xpath`). + /// - Returns: A flattened array containing all matching `Element` instances. + /// - Throws: An error if the WebDriver request fails. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - private func findElements(_ locatorType: LocatorType) async throws -> Elements { + func findElements(_ locatorType: LocatorType) async throws -> Elements { var elements = [Elements]() try await withThrowingTaskGroup(of: Elements.self) { group in for element in self { @@ -94,9 +126,20 @@ public extension Elements { return elements.flatMap(\.self) } + /// Waits until at least one matching descendant element is found within each element in the collection. + /// + /// This method retries the search until a match is found, the retry count is exceeded, + /// or an error other than `noSuchElement` is encountered. + /// + /// - Parameters: + /// - locatorType: The method by which to locate the element. + /// - retryCount: The maximum number of retries before giving up. Defaults to `3`. + /// - durationSeconds: The delay between retries in seconds. Defaults to `1`. + /// - Returns: `true` if an element was found within the retry limit, otherwise `false`. + /// - Throws: Any error thrown by `findElement(_:)` that is not a `noSuchElement` Selenium error. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult - private func waitUntil( + func waitUntil( _ locatorType: LocatorType, retryCount: Int = 3, durationSeconds: Int = 1 diff --git a/Sources/SwiftWebDriver/Error/SeleniumError.swift b/Sources/SwiftWebDriver/Error/SeleniumError.swift index b42f515..58f0343 100644 --- a/Sources/SwiftWebDriver/Error/SeleniumError.swift +++ b/Sources/SwiftWebDriver/Error/SeleniumError.swift @@ -2,13 +2,14 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. import Foundation -/// https://www.selenium.dev/documentation/legacy/json_wire_protocol/#error-handling +/// Represents the types of errors defined by the Selenium JSON Wire Protocol. +/// +/// See [Selenium JSON Wire Protocol Error +/// Handling](https://www.selenium.dev/documentation/legacy/json_wire_protocol/#error-handling) +/// for reference. public enum SeleniumErrorType: String, Codable { case elementClickIntercepted = "element click intercepted" case elementNotInteractable = "element not interactable" @@ -27,40 +28,57 @@ public enum SeleniumErrorType: String, Codable { case scriptTimeout = "script timeout" case sessionNotCreated = "session not created" case staleElementReference = "stale element reference" - case timeout = "timeout" + case timeout case unableToSetCookie = "unable to set cookie" case unexpectedAlertOpen = "unexpected alert open" case unknownCommand = "unknown command" case unknownError = "unknown error" - case unsupportedOperation = "unsupported operation" - - /// Fallback for unknown errors + /// Fallback value for unrecognized or unknown errors. case unrecognized + case unsupportedOperation = "unsupported operation" } +/// Represents a Selenium error returned from the WebDriver server. public struct SeleniumError: Codable, LocalizedError { + /// The detailed error information returned by the WebDriver server. public let value: Value + /// A localized description of the error combining the error type and message. public var errorDescription: String? { "error : \(errorType), message: \(value.message)" } + /// The `SeleniumErrorType` corresponding to the error. + /// + /// Returns `.unrecognized` if the error string does not match any known Selenium error. public var errorType: SeleniumErrorType { SeleniumErrorType(rawValue: value.error) ?? .unrecognized } + /// Encapsulates the raw error data returned by the WebDriver server. public struct Value: Codable, Sendable { + /// The human-readable error message. public let message: String + + /// The raw error string returned by Selenium. public let error: String } } public extension Error { + /// Checks whether the error is a `SeleniumError` of a specific type. + /// + /// - Parameter type: The Selenium error type to check for. + /// - Returns: `true` if the error is a `SeleniumError` matching the specified type, otherwise `false`. func isSeleniumError(ofType type: SeleniumErrorType) -> Bool { guard let seleniumError = self as? SeleniumError else { return false } return seleniumError.errorType == type } + /// Casts the error to a `SeleniumError` of a specific type. + /// + /// - Parameter type: The Selenium error type to match. + /// - Returns: A `SeleniumError` if the error matches the type, otherwise `nil`. func asSeleniumError(ofType type: SeleniumErrorType) -> SeleniumError? { guard let seleniumError = self as? SeleniumError, seleniumError.errorType == type else { return nil } diff --git a/Sources/SwiftWebDriver/WebDriver.swift b/Sources/SwiftWebDriver/WebDriver.swift index ed40856..087b275 100644 --- a/Sources/SwiftWebDriver/WebDriver.swift +++ b/Sources/SwiftWebDriver/WebDriver.swift @@ -6,11 +6,16 @@ import Foundation import NIOCore +/// A high-level wrapper around a `Driver` instance to interact with a WebDriver session. +/// +/// Provides convenient methods for browser navigation, element interaction, JavaScript execution, +/// and session management. public class WebDriver { private let driver: T - /// init webDriver - /// - Parameter driver:Driver + /// Initializes a new `WebDriver` instance. + /// + /// - Parameter driver: The underlying driver conforming to `Driver` protocol. public required init(driver: T) { self.driver = driver } @@ -19,90 +24,127 @@ public class WebDriver { // Clean up resources if needed } - /// webdriver start method - /// - Returns: session id + /// Starts the WebDriver session. + /// + /// - Returns: The session identifier string. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func start() async throws -> String { try await driver.start() } - /// webdriver stop method - /// - Returns: nil + /// Stops the WebDriver session. + /// + /// - Returns: Optional string indicating stopped session, if applicable. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func stop() async throws -> String? { try await driver.stop() } - /// webdriver status - /// - Returns: StatusResponse + /// Retrieves the current status of the WebDriver session. + /// + /// - Returns: A `StatusResponse` object describing the session status. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func status() async throws -> StatusResponse { try await driver.status() } + /// Retrieves the navigation information of the current session. + /// + /// - Returns: A `GetNavigationResponse` object. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func getNavigation() async throws -> GetNavigationResponse { try await driver.getNavigation() } - /// load page - /// - Parameter url: load page url - /// - Returns: PostNavigationResponse + /// Navigates to a specified URL string. + /// + /// - Parameter urlString: The URL to load. + /// - Returns: A `PostNavigationResponse` indicating the result of the navigation. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func navigateTo(urlString: String) async throws -> PostNavigationResponse { try await driver.postNavigation(requestURL: urlString) } + /// Navigates to a specified URL. + /// + /// - Parameter url: The `URL` to load. + /// - Returns: A `PostNavigationResponse` indicating the result of the navigation. @discardableResult public func navigateTo(url: URL) async throws -> PostNavigationResponse { try await navigateTo(urlString: url.absoluteString) } - /// navigation back - /// - Returns: PostNavigationBackResponse + /// Navigates back in the browser history. + /// + /// - Returns: A `PostNavigationBackResponse`. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func navigationBack() async throws -> PostNavigationBackResponse { try await driver.postNavigationBack() } - /// navigation forward - /// - Returns: PostNavigationForwardResponse + /// Navigates forward in the browser history. + /// + /// - Returns: A `PostNavigationForwardResponse`. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func navigationForward() async throws -> PostNavigationForwardResponse { try await driver.postNavigationForward() } + /// Refreshes the current page. + /// + /// - Returns: A `PostNavigationRefreshResponse`. @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func navigationRefresh() async throws -> PostNavigationRefreshResponse { try await driver.postNavigationRefresh() } + /// Retrieves the title of the current page. + /// + /// - Returns: A `GetNavigationTitleResponse`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func navigationTitle() async throws -> GetNavigationTitleResponse { try await driver.getNavigationTitle() } + /// Finds a single element in the current page. + /// + /// - Parameter locatorType: The locator strategy to find the element. + /// - Returns: An `Element` if found. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func findElement(_ locatorType: LocatorType) async throws -> Element { try await driver.findElement(locatorType) } + /// Finds multiple elements in the current page. + /// + /// - Parameter locatorType: The locator strategy to find the elements. + /// - Returns: An array of `Element` objects. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func findElements(_ locatorType: LocatorType) async throws -> Elements { try await driver.findElements(locatorType) } + /// Takes a screenshot of the current page. + /// + /// - Returns: A base64-encoded PNG string. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func screenshot() async throws -> String { try await driver.getScreenShot() } + /// Waits until an element matching the locator appears. + /// + /// - Parameters: + /// - locatorType: The locator to wait for. + /// - retryCount: Number of retry attempts. + /// - durationSeconds: Delay between retries in seconds. + /// - Returns: `true` if the element appears, `false` otherwise. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func waitUntil( @@ -113,6 +155,13 @@ public class WebDriver { try await driver.waitUntil(locatorType, retryCount: retryCount, durationSeconds: durationSeconds) } + /// Executes JavaScript in the context of the page. + /// + /// - Parameters: + /// - script: JavaScript code to execute. + /// - args: Arguments to pass to the script. + /// - type: Execution type (sync or async). + /// - Returns: A `PostExecuteResponse` with the result. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func execute( @@ -123,27 +172,53 @@ public class WebDriver { try await driver.execute(script, args: args, type: type) } + /// Retrieves the currently active element in the page. + /// + /// - Returns: The `Element` that is currently active. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func getActiveElement() async throws -> Element { try await driver.getActiveElement() } + /// Sets an attribute on an element. + /// + /// - Parameters: + /// - element: The target element. + /// - attributeName: The attribute name to set. + /// - newValue: The new value to assign. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func setAttribute(element: Element, attributeName: String, newValue: String) async throws { try await driver.setAttribute(element: element, attributeName: attributeName, newValue: newValue) } + /// Retrieves a property of an element. + /// + /// - Parameters: + /// - element: The target element. + /// - propertyName: The property name to retrieve. + /// - Returns: A `PostExecuteResponse` with the value. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func getProperty(element: Element, propertyName: String) async throws -> PostExecuteResponse { try await driver.getProperty(element: element, propertyName: propertyName) } + /// Sets a property of an element. + /// + /// - Parameters: + /// - element: The target element. + /// - propertyName: The property name to set. + /// - newValue: The value to assign. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func setProperty(element: Element, propertyName: String, newValue: String) async throws { try await driver.setProperty(element: element, propertyName: propertyName, newValue: newValue) } + /// Drags an element and drops it onto a target element. + /// + /// - Parameters: + /// - source: The element to drag. + /// - target: The element to drop onto. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func dragAndDrop(from source: Element, to target: Element) async throws { try await driver.dragAndDrop(from: source, to: target) diff --git a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriver.swift b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriver.swift index 1573c8e..3281da4 100644 --- a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriver.swift +++ b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriver.swift @@ -249,9 +249,10 @@ public class ChromeDriver: Driver { throw WebDriverError.sessionIdIsNil } - let request = PostExecuteSyncRequest( + let request = PostExecuteRequest( baseURL: url, sessionId: sessionId, + type: .sync, javascriptSnippet: .init(script: script, args: args) ) @@ -263,9 +264,10 @@ public class ChromeDriver: Driver { throw WebDriverError.sessionIdIsNil } - let request = PostExecuteAsyncRequest( + let request = PostExecuteRequest( baseURL: url, sessionId: sessionId, + type: .async, javascriptSnippet: .init(script: script, args: args) ) @@ -319,9 +321,7 @@ public class ChromeDriver: Driver { return try await execute(script, args: args, type: .sync) } - public func setProperty(element: Element, propertyName: String, - newValue: String) async throws - { + public func setProperty(element: Element, propertyName: String, newValue: String) async throws { let script = "arguments[0][arguments[1]] = arguments[2];" let args: [AnyEncodable] = [ AnyEncodable(["element-6066-11e4-a52e-4f735466cecf": element.elementId]), diff --git a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift index a983456..4864d07 100644 --- a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift +++ b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift @@ -3,17 +3,48 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +/// Handles drag-and-drop operations between two elements in a ChromeDriver session. +/// +/// `ChromeDriverElementDragAndDropper` determines whether an element is HTML5-draggable. +/// If it is, it simulates the drag-and-drop using JavaScript events. Otherwise, it +/// falls back to the standard WebDriver `dragAndDrop` method. +/// +/// This struct is intended for internal use with ChromeDriver automation. internal struct ChromeDriverElementDragAndDropper { - let source: Element - let target: Element - let driver: ChromeDriver + // MARK: - Properties + /// The source element to drag. + private let source: Element + + /// The target element to drop onto. + private let target: Element + + /// The ChromeDriver instance used to execute JavaScript or WebDriver commands. + private let driver: ChromeDriver + + // MARK: - Initializer + + /// Creates a new drag-and-drop helper for the given elements and driver. + /// + /// - Parameters: + /// - driver: The ChromeDriver instance that will perform the drag-and-drop. + /// - source: The element to be dragged. + /// - target: The element to drop onto. public init(driver: ChromeDriver, from source: Element, to target: Element) { self.driver = driver self.source = source self.target = target } + // MARK: - Public Methods + + /// Performs the drag-and-drop operation between the source and target elements. + /// + /// If the source element is HTML5-draggable, the drag-and-drop is simulated + /// using JavaScript. Otherwise, it uses the standard WebDriver `dragAndDrop`. + /// + /// - Throws: Any errors thrown during attribute lookup, JavaScript execution, + /// or WebDriver drag-and-drop operations. public func dragAndDrop() async throws { if try await isHTML5Draggable() { try await simulateHTML5DragAndDrop() @@ -22,6 +53,14 @@ internal struct ChromeDriverElementDragAndDropper { } } + // MARK: - Private Methods + + /// Simulates an HTML5 drag-and-drop operation using JavaScript events. + /// + /// Dispatches `dragstart`, `dragover`, and `drop` events on the source and target + /// elements to mimic a user performing drag-and-drop in the browser. + /// + /// - Throws: Any errors thrown by the ChromeDriver JavaScript execution. private func simulateHTML5DragAndDrop() async throws { let script = """ function simulateHTML5DragAndDrop(source, target) { @@ -57,6 +96,14 @@ internal struct ChromeDriverElementDragAndDropper { try await driver.execute(script, args: arguments, type: .sync) } + /// Determines whether the source element is HTML5-draggable. + /// + /// Checks the `draggable` attribute of the element and returns `true` if its + /// value is `"true"` (case-insensitive). Returns `false` if the attribute + /// is missing or has a different value. + /// + /// - Returns: `true` if the element supports HTML5 drag-and-drop; otherwise `false`. + /// - Throws: Any errors thrown when accessing the element's attributes via WebDriver. private func isHTML5Draggable() async throws -> Bool { guard let draggableValue = try? await source.attribute(name: "draggable") diff --git a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeOptions.swift b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeOptions.swift index 35c8a4d..acd9f19 100644 --- a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeOptions.swift +++ b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeOptions.swift @@ -2,30 +2,34 @@ // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. -// -// This package is freely distributable under the MIT license. -// This Package is a modified fork of https://github.com/ashi-psn/SwiftWebDriver. import Foundation import NIOCore -public protocol StatableObject: Codable {} - +/// Typealias for a single ChromeOptions argument public typealias Args = String -public struct ChromeOptions: StatableObject { +/// ChromeOptions data structure, used to configure Chrome Driver instantiation +public struct ChromeOptions: Codable { + /// Chrome arguments public let args: [Args]? + /// Initialize ChromeOptions + /// - Parameter args: an array of arguments public init(args: [Args]?) { self.args = args } } +/// Restrict arguments to an enum of allowed arguments public extension Args { + /// Initialize chrome option argument init(_ args: Arguments) { self.init(describing: args) } + /// All allowed arguments, with their flag value coupled (research Chrome Driver argument definitions to know what + /// each argument means) enum Arguments: CustomStringConvertible, Codable { case headless case noSandbox @@ -41,6 +45,7 @@ public extension Args { case proxyServer(proxyURL: String) case userDataDir(dir: String) + /// Case descriptions public var description: String { switch self { case .headless: diff --git a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift index 28618bc..c457eb2 100644 --- a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift +++ b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift @@ -8,14 +8,18 @@ import Testing @Suite("Chrome Driver Drag and Drop Integration Tests", .serialized) internal class ChromeDriverDragAndDropIntegrationTests: ChromeDriverTest { + /// Drag and drop draggable element to another element, testing the `dragAndDrop` method. + /// This test tests the JavaScript script executed in the DOM to drag the draggable element to the other element. @Test("Drag Element To Another (JavaScript)") - func dragAndDropElementToAnotherWithDraggableElement() async throws { + public func dragAndDropDraggableElementToAnother() async throws { page = "dragTarget.html" try await dragAndDrop() } + /// Drag and drop draggable element to another element, testing the `dragAndDrop` method. + /// This test tests the Actions API request to drag the draggable element to the other element. @Test("Drag Element To Another (WebDriver Actions API)") - func dragAndDropElementToAnother() async throws { + public func dragAndDropElementToAnother() async throws { page = "dragBox.html" try await dragAndDrop() } diff --git a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift index c5fb9d1..8a1aea6 100644 --- a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift +++ b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift @@ -9,7 +9,7 @@ import Testing @Suite("Chrome Driver Element Handles", .serialized) internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { @Test("Click Button") - func clickButton() async throws { + public func clickButton() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) @@ -20,7 +20,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Double Click Button") - func doubleClickButton() async throws { + public func doubleClickButton() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) @@ -31,7 +31,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Drag Element To Another") - func dragElementToAnother() async throws { + public func dragElementToAnother() async throws { page = "dragBox.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) @@ -45,7 +45,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Get Element Attributes") - func getAttribute() async throws { + public func getAttribute() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) @@ -56,7 +56,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Get Element Rect") - func getRect() async throws { + public func getRect() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) @@ -66,13 +66,13 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { let rect = try await element.rect() #expect(rect.height == 100) - #expect(rect.x > 5) - #expect(rect.y > 5) + #expect(rect.xPosition > 5) + #expect(rect.yPosition > 5) #expect(rect.width == 100) } @Test("Clear Element") - func clearElement() async throws { + public func clearElement() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) @@ -86,7 +86,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Send Key") - func sendKey() async throws { + public func sendKey() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) let element = try await driver.findElement(.css(.id("sendValue"))) @@ -96,7 +96,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Get Screenshot") - func getScreenshot() async throws { + public func getScreenshot() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) let element = try await driver.findElement(.css(.id("sendValue"))) @@ -106,7 +106,7 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { } @Test("Fail any operation if element becomes stale") - func throwStaleError() async throws { + public func throwStaleError() async throws { let sleepTotal = 3 page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) diff --git a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverPropertyIntegrationTests.swift b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverPropertyIntegrationTests.swift index 96f9022..2c594ba 100644 --- a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverPropertyIntegrationTests.swift +++ b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverPropertyIntegrationTests.swift @@ -8,8 +8,9 @@ import Testing @Suite("Chrome Driver Property Integration Tests", .serialized) internal class ChromeDriverPropertyIntegrationTests: ChromeDriverTest { + /// Test `getProperty()` method, a method to get a specific property value from an element. @Test("Get Property") - func getProperty() async throws { + public func getProperty() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) let updatedElementValue = "NewElementValue" @@ -28,8 +29,9 @@ internal class ChromeDriverPropertyIntegrationTests: ChromeDriverTest { #expect(elementValue == updatedElementValue) } + /// Test `setProperty()` method, a method to set a specific property value from an element. @Test("Set Property") - func setProperty() async throws { + public func setProperty() async throws { page = "elementHandleTestPage.html" try await driver.navigateTo(urlString: testPageURL.absoluteString) let element = try await driver.findElement(.css(.id("setproperty"))) diff --git a/package.json b/package.json index 86525e1..8f94bc3 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,14 @@ "description": "> [!NOTE] This codebase is a fork made from this [REPO](https://github.com/ashi-psn/SwiftWebDriver). The majority of the code was written by @ashi-psn. This repo just modernizes it & adapts it for our organizations usecases.", "main": "index.js", "scripts": { + "test": "npm run compose:down ; npm run compose:up:integration-services ; swift build ; swift test", "format": "swiftformat .", "lint": "swiftlint .", "install:all": "npx npm-run-all install:swiftformat install:swiftlint config", "install:swiftformat": "brew install swiftformat", "install:swiftlint": "brew install swiftlint", "compose:up": "npm run compose -- up -d", + "compose:up:integration-services": "npm run compose:up -- --wait selenium httpd", "compose:build": "npm run compose -- build", "compose:build:no-cache": "npm run compose -- build --no-cache", "compose:build-and-up": "npx npm-run-all --sequential compose:build compose:up",