Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/WebDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ add_library(WebDriver
Poll.swift
Request.swift
Requests.swift
Requests+LegacySelenium.swift
Requests+W3C.swift
ScreenOrientation.swift
Session.swift
TimeoutType.swift
Expand Down
38 changes: 33 additions & 5 deletions Sources/WebDriver/HTTPWebDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,60 @@ import Foundation
import FoundationNetworking
#endif

/// Thrown if we fail to detect the protocol a webdriver server uses.
public struct ProtocolDetectionError: Error {}

/// A connection to a WebDriver server over HTTP.
public struct HTTPWebDriver: WebDriver {
let rootURL: URL
private let serverURL: URL
public let wireProtocol: WireProtocol

public static let defaultRequestTimeout: TimeInterval = 5 // seconds

public init(endpoint: URL, wireProtocol: WireProtocol) {
rootURL = endpoint
serverURL = endpoint
self.wireProtocol = wireProtocol
}

public static func createWithDetectedProtocol(serverURL: URL) throws -> HTTPWebDriver {
Comment thread
tristanlabelle marked this conversation as resolved.
.init(endpoint: serverURL, wireProtocol: try detectProtocol(serverURL: serverURL))
}

public static func detectProtocol(serverURL: URL) throws -> WireProtocol {
Comment thread
tristanlabelle marked this conversation as resolved.
// The status request is the same for the Selenium Legacy JSON protocol and W3C,
// but the response format is different.
let urlRequest = try Self.buildURLRequest(serverURL: serverURL, Requests.LegacySelenium.Status())

// Send the request and decode result or error
let (status, responseData) = try urlRequest.send()
guard status == 200 else {
throw try JSONDecoder().decode(ErrorResponse.self, from: responseData)
}

if let _ = try? JSONDecoder().decode(Requests.LegacySelenium.Status.Response.self, from: responseData) {
return .legacySelenium
} else if let _ = try? JSONDecoder().decode(Requests.W3C.Status.Response.self, from: responseData) {
return .w3c
} else {
throw ProtocolDetectionError()
}
}

@discardableResult
public func send<Req: Request>(_ request: Req) throws -> Req.Response {
let urlRequest = try buildURLRequest(request)
let urlRequest = try Self.buildURLRequest(serverURL: self.serverURL, request)

// Send the request and decode result or error
let (status, responseData) = try urlRequest.send()
guard status == 200 else {
throw try JSONDecoder().decode(ErrorResponse.self, from: responseData)
}

return try JSONDecoder().decode(Req.Response.self, from: responseData)
}

private func buildURLRequest<Req: Request>(_ request: Req) throws -> URLRequest {
var url = rootURL
private static func buildURLRequest<Req: Request>(serverURL: URL, _ request: Req) throws -> URLRequest {
var url = serverURL
for (index, pathComponent) in request.pathComponents.enumerated() {
let last = index == request.pathComponents.count - 1
url.appendPathComponent(pathComponent, isDirectory: !last)
Expand Down
37 changes: 37 additions & 0 deletions Sources/WebDriver/Requests+LegacySelenium.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
extension Requests {
/// Defines requests and response types specific to the legacy selenium json wire protocol.
public enum LegacySelenium {
// https://www.selenium.dev/documentation/legacy/json_wire_protocol/#session
public struct Session<Caps: Capabilities>: Request {
public var desiredCapabilities: Caps
public var requiredCapabilities: Caps?

public init(desiredCapabilities: Caps, requiredCapabilities: Caps? = nil) {
self.requiredCapabilities = requiredCapabilities
self.desiredCapabilities = desiredCapabilities
}

public var pathComponents: [String] { ["session"] }
public var method: HTTPMethod { .post }
public var body: Body { .init(desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities) }

public struct Body: Codable {
public var desiredCapabilities: Caps
public var requiredCapabilities: Caps?
}

public struct Response: Codable {
public var sessionId: String
public var value: Caps
}
}

// https://www.selenium.dev/documentation/legacy/json_wire_protocol/#status
public struct Status: Request {
public var pathComponents: [String] { ["status"] }
public var method: HTTPMethod { .get }

public typealias Response = WebDriverStatus
}
}
}
41 changes: 41 additions & 0 deletions Sources/WebDriver/Requests+W3C.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
extension Requests {
/// Defines requests and response types specific to the W3C WebDriver protocol.
public enum W3C {
public struct Session<Caps: Capabilities>: Request {
public var alwaysMatch: Caps
public var firstMatch: [Caps]

public init(alwaysMatch: Caps, firstMatch: [Caps] = []) {
self.alwaysMatch = alwaysMatch
self.firstMatch = firstMatch
}

public var pathComponents: [String] { ["session"] }
public var method: HTTPMethod { .post }
public var body: Body { .init(capabilities: .init(alwaysMatch: alwaysMatch, firstMatch: firstMatch)) }

public struct Body: Codable {
public struct Capabilities: Codable {
public var alwaysMatch: Caps
public var firstMatch: [Caps]?
}

public var capabilities: Capabilities
}

public typealias Response = ResponseWithValue<ResponseValue>

public struct ResponseValue: Codable {
public var sessionId: String
public var capabilities: Caps
}
}

public struct Status: Request {
public var pathComponents: [String] { ["status"] }
public var method: HTTPMethod { .get }

public typealias Response = ResponseWithValue<WebDriverStatus>
}
}
}
63 changes: 1 addition & 62 deletions Sources/WebDriver/Requests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// Defines request and response types for the WebDriver protocol.
public enum Requests {
public struct ResponseWithValue<Value>: Codable where Value: Codable {
public var value: Value
Expand Down Expand Up @@ -160,60 +161,6 @@ public enum Requests {
public typealias Response = ResponseWithValue<String>
}

public struct Session_Legacy<Caps: Capabilities>: Request {
public var desiredCapabilities: Caps
public var requiredCapabilities: Caps?

public init(desiredCapabilities: Caps, requiredCapabilities: Caps? = nil) {
self.requiredCapabilities = requiredCapabilities
self.desiredCapabilities = desiredCapabilities
}

public var pathComponents: [String] { ["session"] }
public var method: HTTPMethod { .post }
public var body: Body { .init(desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities) }

public struct Body: Codable {
public var desiredCapabilities: Caps
public var requiredCapabilities: Caps?
}

public struct Response: Codable {
public var sessionId: String
public var value: Caps
}
}

public struct Session_W3C<Caps: Capabilities>: Request {
public var alwaysMatch: Caps
public var firstMatch: [Caps]

public init(alwaysMatch: Caps, firstMatch: [Caps] = []) {
self.alwaysMatch = alwaysMatch
self.firstMatch = firstMatch
}

public var pathComponents: [String] { ["session"] }
public var method: HTTPMethod { .post }
public var body: Body { .init(capabilities: .init(alwaysMatch: alwaysMatch, firstMatch: firstMatch)) }

public struct Body: Codable {
public struct Capabilities: Codable {
public var alwaysMatch: Caps
public var firstMatch: [Caps]?
}

public var capabilities: Capabilities
}

public typealias Response = ResponseWithValue<ResponseValue>

public struct ResponseValue: Codable {
public var sessionId: String
public var capabilities: Caps
}
}

// https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementactive
public struct SessionActiveElement: Request {
public var session: String
Expand Down Expand Up @@ -662,14 +609,6 @@ public enum Requests {
public typealias Response = ResponseWithValue<String>
}

// https://www.selenium.dev/documentation/legacy/json_wire_protocol/#status
public struct Status: Request {
public var pathComponents: [String] { ["status"] }
public var method: HTTPMethod { .get }

public typealias Response = WebDriverStatus
}

// https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidorientation
public enum SessionOrientation {
public struct Post: Request {
Expand Down
35 changes: 29 additions & 6 deletions Sources/WebDriver/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// Represents a session in the WebDriver protocol,
/// which manages the lifetime of a page or app under UI automation.
public class Session {
public final class Session {
public let webDriver: any WebDriver
public let id: String
public let capabilities: Capabilities
Expand All @@ -20,8 +20,9 @@ public class Session {
self.shouldDelete = owned
}

public convenience init(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities? = nil) throws {
let response = try webDriver.send(Requests.Session_Legacy(
/// Initializer for Legacy Selenium JSON Protocol
fileprivate convenience init(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities?) throws {
let response = try webDriver.send(Requests.LegacySelenium.Session(
desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities))
self.init(
webDriver: webDriver,
Expand All @@ -30,16 +31,38 @@ public class Session {
owned: true)
}

public static func createW3C(webDriver: any WebDriver, alwaysMatch: Capabilities, firstMatch: [Capabilities] = []) throws -> Session {
let response = try webDriver.send(Requests.Session_W3C(
/// Initializer for W3C Protocol
fileprivate convenience init(webDriver: any WebDriver, alwaysMatch: Capabilities, firstMatch: [Capabilities]) throws {
let response = try webDriver.send(Requests.W3C.Session(
alwaysMatch: alwaysMatch, firstMatch: firstMatch))
return Session(
self.init(
webDriver: webDriver,
existingId: response.value.sessionId,
capabilities: response.value.capabilities,
owned: true)
}

public convenience init(webDriver: any WebDriver, capabilities: Capabilities) throws {
Comment thread
tristanlabelle marked this conversation as resolved.
switch webDriver.wireProtocol {
case .legacySelenium:
try self.init(webDriver: webDriver, desiredCapabilities: capabilities, requiredCapabilities: capabilities)
case .w3c:
try self.init(webDriver: webDriver, alwaysMatch: capabilities, firstMatch: [])
}
}

public enum LegacySelenium {
public static func create(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities? = nil) throws -> Session {
try Session(webDriver: webDriver, desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities)
}
}

public enum W3C {
public static func create(webDriver: any WebDriver, alwaysMatch: Capabilities, firstMatch: [Capabilities] = []) throws -> Session {
try Session(webDriver: webDriver, alwaysMatch: alwaysMatch, firstMatch: firstMatch)
}
}

/// The amount of time the driver should implicitly wait when searching for elements.
/// This functionality is either implemented by the driver, or emulated by swift-webdriver as a fallback.
public var implicitWaitTimeout: TimeInterval {
Expand Down
11 changes: 8 additions & 3 deletions Sources/WebDriver/WebDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ public protocol WebDriver {

extension WebDriver {
/// status - returns WinAppDriver status
/// Returns: an instance of the Status type
/// Returns: an instance of the WebDriverStatus type
public var status: WebDriverStatus {
get throws { try send(Requests.Status()) }
get throws {
switch wireProtocol {
case .legacySelenium: return try send(Requests.LegacySelenium.Status())
case .w3c: return try send(Requests.W3C.Status()).value
}
}
}

public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool { false }
}
}
4 changes: 2 additions & 2 deletions Tests/AppiumTests/AppiumTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ final class AppiumTests: XCTestCase {
capabilities.platformName = "windows"
capabilities.appiumOptions = appiumOptions

let session = try Session.createW3C(webDriver: webDriver, alwaysMatch: capabilities)
let session = try Session(webDriver: webDriver, capabilities: capabilities)
try session.sendKeys(.alt(.f4))
}
#endif
}
}
28 changes: 25 additions & 3 deletions Tests/UnitTests/APIToRequestMappingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,41 @@ class APIToRequestMappingTests: XCTestCase {

func testCreateSession() throws {
let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium)
mockWebDriver.expect(path: "session", method: .post, type: Requests.Session_Legacy.self) {
mockWebDriver.expect(path: "session", method: .post, type: Requests.LegacySelenium.Session.self) {
let capabilities = Capabilities()
capabilities.platformName = "myPlatform"
return Requests.Session_Legacy.Response(sessionId: "mySession", value: capabilities)
return Requests.LegacySelenium.Session.Response(sessionId: "mySession", value: capabilities)
}
let session = try Session(webDriver: mockWebDriver, desiredCapabilities: Capabilities())
let session = try Session(webDriver: mockWebDriver, capabilities: Capabilities())
XCTAssertEqual(session.id, "mySession")
XCTAssertEqual(session.capabilities.platformName, "myPlatform")

// Account for session deinitializer
mockWebDriver.expect(path: "session/mySession", method: .delete)
}

func testStatus_legacySelenium() throws {
let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium)
mockWebDriver.expect(path: "status", method: .get, type: Requests.LegacySelenium.Status.self) {
var status = WebDriverStatus()
status.ready = true
return status
}

XCTAssertEqual(try mockWebDriver.status.ready, true)
}

func testStatus_w3c() throws {
let mockWebDriver = MockWebDriver(wireProtocol: .w3c)
mockWebDriver.expect(path: "status", method: .get, type: Requests.W3C.Status.self) {
var status = WebDriverStatus()
status.ready = true
return Requests.W3C.Status.Response(status)
}

XCTAssertEqual(try mockWebDriver.status.ready, true)
}

func testSessionTitle() throws {
let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium)
let session = Session(webDriver: mockWebDriver, existingId: "mySession")
Expand Down
2 changes: 1 addition & 1 deletion Tests/WinAppDriverTests/MSInfo32App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class MSInfo32App {

init(winAppDriver: WinAppDriver) throws {
let capabilities = WinAppDriver.Capabilities.startApp(name: "\(WindowsSystemPaths.system32)\\msinfo32.exe")
session = try Session(webDriver: winAppDriver, desiredCapabilities: capabilities, requiredCapabilities: capabilities)
session = try Session(webDriver: winAppDriver, capabilities: capabilities)
session.implicitWaitTimeout = 1
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/WinAppDriverTests/TimeoutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class TimeoutTests: XCTestCase {
// Use a simple app in which we can expect queries to execute quickly
let capabilities = WinAppDriver.Capabilities.startApp(
name: "\(WindowsSystemPaths.system32)\\winver.exe")
return try Session(webDriver: winAppDriver, desiredCapabilities: capabilities)
return try Session(webDriver: winAppDriver, capabilities: capabilities)
}

static func measureTime(_ callback: () throws -> Void) rethrows -> Double {
Expand Down