diff --git a/.dotfiles b/.dotfiles index 3de6f4f..ec9bc76 160000 --- a/.dotfiles +++ b/.dotfiles @@ -1 +1 @@ -Subproject commit 3de6f4f94f7c3e2c2a7b43f83bc1da3748678f7d +Subproject commit ec9bc762fe9f4a14eee111d5ae65f02dab540b0b diff --git a/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift b/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift new file mode 100644 index 0000000..dd97786 --- /dev/null +++ b/Sources/SwiftWebDriver/API/Request/ActionsPayload.swift @@ -0,0 +1,52 @@ +// ActionsPayload.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +struct WebDriverElementOrigin: Encodable { + let element: String + + enum CodingKeys: String, CodingKey { + case element = "element-6066-11e4-a52e-4f735466cecf" + } +} + +struct PointerAction: Encodable { + let type: String + let origin: WebDriverElementOrigin? + let x: Int? + let y: Int? + let button: Int? + let duration: Int? + + init( + type: String, + origin: WebDriverElementOrigin? = nil, + x: Int? = nil, + y: Int? = nil, + button: Int? = nil, + duration: Int? = nil + ) { + self.type = type + self.origin = origin + self.x = x + self.y = y + self.button = button + self.duration = duration + } +} + +struct PointerSource: Encodable { + let type: String + let id: String + let parameters: Parameters + let actions: [PointerAction] + + struct Parameters: Encodable { + let pointerType: String + } +} + +struct ActionsPayload: Encodable { + let actions: [PointerSource] +} diff --git a/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift b/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift new file mode 100644 index 0000000..53553cb --- /dev/null +++ b/Sources/SwiftWebDriver/API/Request/Elements/GetElementRectRequest.swift @@ -0,0 +1,22 @@ +// GetElementRectRequest.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 + +internal struct GetElementRectRequest: RequestType { + typealias Response = GetElementRectResponse + + var baseURL: URL + var sessionId: String + var elementId: String + + var path: String { "session/\(sessionId)/element/\(elementId)/rect" } + var method: HTTPMethod = .GET + var headers: HTTPHeaders = [:] + var body: HTTPClient.Body? { nil } +} diff --git a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift index aba8832..c8f92f0 100644 --- a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift +++ b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDoubleClickRequest.swift @@ -58,51 +58,3 @@ internal struct PostElementDoubleClickRequest: RequestType { return .data(data) } } - -struct WebDriverElementOrigin: Encodable { - let element: String - - enum CodingKeys: String, CodingKey { - case element = "element-6066-11e4-a52e-4f735466cecf" - } -} - -struct PointerAction: Encodable { - let type: String - let origin: WebDriverElementOrigin? - let x: Int? - let y: Int? - let button: Int? - let duration: Int? - - init( - type: String, - origin: WebDriverElementOrigin? = nil, - x: Int? = nil, - y: Int? = nil, - button: Int? = nil, - duration: Int? = nil - ) { - self.type = type - self.origin = origin - self.x = x - self.y = y - self.button = button - self.duration = duration - } -} - -struct PointerSource: Encodable { - let type: String - let id: String - let parameters: Parameters - let actions: [PointerAction] - - struct Parameters: Encodable { - let pointerType: String - } -} - -struct ActionsPayload: Encodable { - let actions: [PointerSource] -} diff --git a/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift new file mode 100644 index 0000000..2fb28fa --- /dev/null +++ b/Sources/SwiftWebDriver/API/Request/Elements/PostElementDragAndDropRequest.swift @@ -0,0 +1,73 @@ +// PostElementDragAndDropRequest.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import AnyCodable +import AsyncHTTPClient +import Foundation +import NIO +import NIOHTTP1 + +internal struct PostElementDragAndDropRequest: RequestType { + typealias Response = PostElementClickResponse + + var baseURL: URL + + var sessionId: String + + var elementId: String + + var toElementId: String + + var elementRect: ElementRect + + var targetElementRect: ElementRect + + var path: String { + "session/\(sessionId)/actions" + } + + var method: HTTPMethod = .POST + + var headers: HTTPHeaders = [:] + + var body: HTTPClient.Body? { + let origin = WebDriverElementOrigin(element: elementId) + let dragToOrigin = WebDriverElementOrigin(element: toElementId) + + // let sourceCenterX = Int(elementRect.width / 2) + // let sourceCenterY = Int(elementRect.height / 2) + // + let targetCenterX = Int(targetElementRect.width / 2) + let targetCenterY = Int(targetElementRect.height / 2) + + let pointerActions = [ + // PointerAction(type: "pointerMove", origin: origin, x: sourceCenterX, y: sourceCenterY), + 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) + ] + + let pointerSource = PointerSource( + type: "pointer", + id: "mouse", + parameters: .init(pointerType: "mouse"), + actions: pointerActions + ) + + let payload = ActionsPayload(actions: [pointerSource]) + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let data = try? encoder.encode(payload) + + guard let data else { + return nil + } + + return .data(data) + } +} diff --git a/Sources/SwiftWebDriver/API/Response/Elements/GetElementRectResponse.swift b/Sources/SwiftWebDriver/API/Response/Elements/GetElementRectResponse.swift new file mode 100644 index 0000000..13e34e2 --- /dev/null +++ b/Sources/SwiftWebDriver/API/Response/Elements/GetElementRectResponse.swift @@ -0,0 +1,10 @@ +// GetElementRectResponse.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import Foundation + +public struct GetElementRectResponse: ResponseType { + public let value: ElementRect +} diff --git a/Sources/SwiftWebDriver/Element/Element.swift b/Sources/SwiftWebDriver/Element/Element.swift index 1a5b25e..a6fcb0c 100644 --- a/Sources/SwiftWebDriver/Element/Element.swift +++ b/Sources/SwiftWebDriver/Element/Element.swift @@ -17,10 +17,12 @@ public protocol ElementCommandProtocol: FindElementProtocol { func name() async throws -> String func click() async throws -> String? func doubleClick() async throws -> String? + func dragAndDrop(to: Element) async throws -> String? func clear() async throws -> String? func attribute(name: String) async throws -> String func send(value: String) async throws -> String? func screenshot() async throws -> String + func rect() async throws -> ElementRect } public struct Element: ElementCommandProtocol, Sendable { @@ -85,6 +87,32 @@ public struct Element: ElementCommandProtocol, Sendable { return response.value } + @discardableResult + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func dragAndDrop(to: Element) async throws -> String? { + let request = try await PostElementDragAndDropRequest( + baseURL: baseURL, + sessionId: sessionId, + elementId: elementId, + toElementId: to.elementId, + elementRect: rect(), + targetElementRect: to.rect() + ) + let response = try await APIClient.shared.request(request) + return response.value + } + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func rect() async throws -> ElementRect { + let request = GetElementRectRequest( + baseURL: baseURL, + sessionId: sessionId, + elementId: elementId + ) + let response = try await APIClient.shared.request(request) + return response.value + } + @discardableResult @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func clear() async throws -> String? { diff --git a/Sources/SwiftWebDriver/Element/ElementRect.swift b/Sources/SwiftWebDriver/Element/ElementRect.swift new file mode 100644 index 0000000..f5fd9da --- /dev/null +++ b/Sources/SwiftWebDriver/Element/ElementRect.swift @@ -0,0 +1,11 @@ +// ElementRect.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +public struct ElementRect: Codable, Sendable { + public let x: Double + public let y: Double + public let width: Double + public let height: Double +} diff --git a/Sources/SwiftWebDriver/WebDriver.swift b/Sources/SwiftWebDriver/WebDriver.swift index 6b223b6..ed40856 100644 --- a/Sources/SwiftWebDriver/WebDriver.swift +++ b/Sources/SwiftWebDriver/WebDriver.swift @@ -143,4 +143,9 @@ public class WebDriver { public func setProperty(element: Element, propertyName: String, newValue: String) async throws { try await driver.setProperty(element: element, propertyName: propertyName, newValue: newValue) } + + @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 c77c619..1573c8e 100644 --- a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriver.swift +++ b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriver.swift @@ -332,6 +332,10 @@ public class ChromeDriver: Driver { try await execute(script, args: args, type: .sync) } + public func dragAndDrop(from source: Element, to target: Element) async throws { + try await ChromeDriverElementDragAndDropper(driver: self, from: source, to: target).dragAndDrop() + } + deinit { let url = url let sessionId = sessionId diff --git a/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift new file mode 100644 index 0000000..2721772 --- /dev/null +++ b/Sources/SwiftWebDriver/WebDrivers/Chrome/ChromeDriverElementDragAndDropper.swift @@ -0,0 +1,64 @@ +// ChromeDriverElementDragAndDropper.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +internal struct ChromeDriverElementDragAndDropper { + let source: Element + let target: Element + let driver: ChromeDriver + + public init(driver: ChromeDriver, from source: Element, to target: Element) { + self.driver = driver + self.source = source + self.target = target + } + + public func dragAndDrop() async throws { + if try await isHTML5Draggable() { + try await simulateHTML5DragAndDrop() + } else { + try await source.dragAndDrop(to: target) + } + } + + private func simulateHTML5DragAndDrop() async throws { + let script = """ + function simulateHTML5DragAndDrop(source, target) { + const dataTransfer = new DataTransfer(); + + const dragStartEvent = new DragEvent('dragstart', { + bubbles: true, + cancelable: true, + dataTransfer: dataTransfer + }); + source.dispatchEvent(dragStartEvent); + + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + dataTransfer: dataTransfer + }); + target.dispatchEvent(dragOverEvent); + + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + dataTransfer: dataTransfer + }); + target.dispatchEvent(dropEvent); + } + simulateHTML5DragAndDrop(arguments[0], arguments[1]); + """ + let arguments: [AnyEncodable] = [ + AnyEncodable(["element-6066-11e4-a52e-4f735466cecf": source.elementId]), + AnyEncodable(["element-6066-11e4-a52e-4f735466cecf": target.elementId]) + ] + try await driver.execute(script, args: arguments, type: .sync) + } + + private func isHTML5Draggable() async throws -> Bool { + let draggableValue = try await source.attribute(name: "draggable") + return draggableValue.lowercased() == "true" + } +} diff --git a/Sources/SwiftWebDriver/WebDrivers/Driver.swift b/Sources/SwiftWebDriver/WebDrivers/Driver.swift index 57088b5..47414ce 100644 --- a/Sources/SwiftWebDriver/WebDrivers/Driver.swift +++ b/Sources/SwiftWebDriver/WebDrivers/Driver.swift @@ -52,4 +52,6 @@ public protocol Driver: FindElementProtocol { func getProperty(element: Element, propertyName: String) async throws -> PostExecuteResponse func setProperty(element: Element, propertyName: String, newValue: String) async throws + + func dragAndDrop(from source: Element, to target: Element) async throws } diff --git a/TestAssets/dragBox.html b/TestAssets/dragBox.html new file mode 100644 index 0000000..71bd8ba --- /dev/null +++ b/TestAssets/dragBox.html @@ -0,0 +1,83 @@ + + + + + + Pointer Drag Test + + + + +
SOURCE
+
TARGET
+ + + + + diff --git a/TestAssets/dragTarget.html b/TestAssets/dragTarget.html new file mode 100644 index 0000000..732c424 --- /dev/null +++ b/TestAssets/dragTarget.html @@ -0,0 +1,60 @@ + + + + + + Drag and Drop Test + + + + +
SOURCE
+
TARGET
+ + + + + diff --git a/TestAssets/elementHandleTestPage.html b/TestAssets/elementHandleTestPage.html index 981d2b9..8208f0e 100644 --- a/TestAssets/elementHandleTestPage.html +++ b/TestAssets/elementHandleTestPage.html @@ -28,10 +28,17 @@ element.remove() }, 2000) + +
diff --git a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift new file mode 100644 index 0000000..1ffe790 --- /dev/null +++ b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverDragAndDropIntegrationTests.swift @@ -0,0 +1,47 @@ +// ChromeDriverDragAndDropIntegrationTests.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +@testable import SwiftWebDriver +import Testing + +@Suite("Chrome Driver Drag and Drop Integration Tests", .serialized) +internal class ChromeDriverDragAndDropIntegrationTests: ChromeDriverTest { + @Test("Drag Element To Another") + func dragAndDropElementToAnother() async throws { + page = "dragTarget.html" + try await driver.navigateTo(urlString: testPageURL.absoluteString) + + let sourceElement = try await driver.findElement(.css(.id("source"))) + let targetElement = try await driver.findElement(.css(.id("target"))) + + try await driver.dragAndDrop(from: sourceElement, to: targetElement) + + let targetElementText = try await driver.getProperty(element: targetElement, propertyName: "innerText").value? + .stringValue + + #expect(targetElementText == "DROPPED!") + } + + @Test("Set Property") + func setProperty() async throws { + page = "elementHandleTestPage.html" + try await driver.navigateTo(urlString: testPageURL.absoluteString) + let element = try await driver.findElement(.css(.id("setproperty"))) + let newPropertyValue = "element new inner text" + + try await driver.setProperty(element: element, propertyName: "innerText", newValue: newPropertyValue) + guard + let elementInnerTextValue = try await driver.getProperty(element: element, propertyName: "innerText").value? + .stringValue + else { + #expect(Bool(false), "Could not convert element innerText value to string value") + return + } + + #expect(newPropertyValue == elementInnerTextValue) + } + + deinit {} +} diff --git a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift index 4633017..c5fb9d1 100644 --- a/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift +++ b/Tests/SwiftWebDriverIntegrationTests/ChromeDriver/Element/ChromeDriverElementHandleIntegrationTests.swift @@ -30,6 +30,20 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { #expect(test == "ii") } + @Test("Drag Element To Another") + func dragElementToAnother() async throws { + page = "dragBox.html" + try await driver.navigateTo(urlString: testPageURL.absoluteString) + + let source = try await driver.findElement(.css(.id("source"))) + let target = try await driver.findElement(.css(.id("target"))) + + try await source.dragAndDrop(to: target) + + let targetText = try await driver.getProperty(element: target, propertyName: "innerText").value?.stringValue + #expect(targetText == "DROPPED!", "Target text should be 'DROPPED!' after pointer drag") + } + @Test("Get Element Attributes") func getAttribute() async throws { page = "elementHandleTestPage.html" @@ -41,6 +55,22 @@ internal class ChromeDriverElementHandleIntegrationTests: ChromeDriverTest { #expect(attribute == "expect attribute") } + @Test("Get Element Rect") + func getRect() async throws { + page = "elementHandleTestPage.html" + try await driver.navigateTo(urlString: testPageURL.absoluteString) + + let element = try await driver + .findElement(.css(.id("rect"))) + + let rect = try await element.rect() + + #expect(rect.height == 100) + #expect(rect.x > 5) + #expect(rect.y > 5) + #expect(rect.width == 100) + } + @Test("Clear Element") func clearElement() async throws { page = "elementHandleTestPage.html"