├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .swiftformat ├── .vscode └── tasks.json ├── CMakeLists.txt ├── CONTRIBUTING.md ├── Docs ├── SupportedAPIs.md └── Usage.md ├── License.txt ├── Package.swift ├── Readme.md ├── Sources ├── WebDriver │ ├── CMakeLists.txt │ ├── Capabilities+AppiumOptions.swift │ ├── Capabilities.swift │ ├── Element.swift │ ├── ElementLocator.swift │ ├── ErrorResponse.swift │ ├── HTTPWebDriver.swift │ ├── Keys.swift │ ├── Location.swift │ ├── MouseButton.swift │ ├── NoSuchElementError.swift │ ├── Poll.swift │ ├── Request.swift │ ├── Requests+LegacySelenium.swift │ ├── Requests+W3C.swift │ ├── Requests.swift │ ├── ScreenOrientation.swift │ ├── Session.swift │ ├── TimeoutType.swift │ ├── TouchClickKind.swift │ ├── URLRequestExtensions.swift │ ├── WebDriver.swift │ ├── WebDriverStatus.swift │ ├── Window.swift │ └── WireProtocol.swift └── WinAppDriver │ ├── CMakeLists.txt │ ├── CommandLine.swift │ ├── ElementLocator+accessibilityId.swift │ ├── ErrorResponse+WinAppDriver.swift │ ├── ReexportWebDriver.swift │ ├── Win32Error.swift │ ├── Win32ProcessTree.swift │ ├── WinAppDriver+Attributes.swift │ ├── WinAppDriver+Capabilities.swift │ ├── WinAppDriver.swift │ └── WindowsSystemPaths.swift └── Tests ├── AppiumTests └── AppiumTests.swift ├── Common └── PNGUtilities.swift ├── UnitTests ├── APIToRequestMappingTests.swift └── MockWebDriver.swift └── WinAppDriverTests ├── AppDriverOptionsTest.swift ├── CommandLineTests.swift ├── MSInfo32App.swift ├── RequestsTests.swift └── TimeoutTests.swift /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-spm: 14 | name: Build & Test (SPM) 15 | runs-on: windows-2022 16 | timeout-minutes: 20 17 | 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v4.2.2 21 | 22 | - name: Setup Visual Studio Development Environment 23 | uses: compnerd/gha-setup-vsdevenv@f1ba60d553a3216ce1b89abe0201213536bc7557 # main as of 2024-11-12 24 | with: 25 | winsdk: "10.0.22621.0" # GitHub runners have 10.0.26100.0 which regresses Swift's ucrt module 26 | 27 | - name: Install Swift 28 | uses: compnerd/gha-setup-swift@b6c5fc1ed5b5439ada8e7661985acb09ad8c3ba2 # main as of 2024-11-12 29 | with: 30 | branch: swift-5.8-release 31 | tag: 5.8-RELEASE 32 | 33 | - name: Build 34 | run: swift build --verbose --build-tests 35 | 36 | - name: Test 37 | run: swift test --verbose --skip-build 38 | 39 | build-cmake: 40 | name: Build (CMake) 41 | runs-on: windows-2022 42 | timeout-minutes: 20 43 | 44 | steps: 45 | - name: Checkout repo 46 | uses: actions/checkout@v4.2.2 47 | 48 | - name: Setup Visual Studio Development Environment 49 | uses: compnerd/gha-setup-vsdevenv@f1ba60d553a3216ce1b89abe0201213536bc7557 # main as of 2024-11-12 50 | with: 51 | winsdk: "10.0.22621.0" # GitHub runners have 10.0.26100.0 which regresses Swift's ucrt module 52 | 53 | - name: Install Swift 54 | uses: compnerd/gha-setup-swift@b6c5fc1ed5b5439ada8e7661985acb09ad8c3ba2 # main as of 2024-11-12 55 | with: 56 | branch: swift-5.8-release 57 | tag: 5.8-RELEASE 58 | 59 | - name: CMake Configure 60 | shell: pwsh 61 | run: cmake -S . -B build -G Ninja 62 | 63 | - name: CMake Build 64 | shell: pwsh 65 | run: cmake --build .\build 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /build 4 | /Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/ 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # General Options 2 | --swiftversion 5.9 3 | 4 | # File Options 5 | --exclude .build/ 6 | 7 | # Format Options 8 | 9 | --allman false 10 | --binarygrouping none 11 | --closingparen balanced 12 | --commas always 13 | --conflictmarkers reject 14 | --decimalgrouping none 15 | --elseposition same-line 16 | --exponentcase lowercase 17 | --exponentgrouping disabled 18 | --fractiongrouping disabled 19 | --fragment false 20 | --header ignore 21 | --hexgrouping none 22 | --hexliteralcase lowercase 23 | --ifdef no-indent 24 | --importgrouping alphabetized 25 | --indent 4 26 | --indentcase false 27 | # make sure this matches .swiftlint 28 | --maxwidth 180 29 | --nospaceoperators ..<,... 30 | --octalgrouping none 31 | --operatorfunc no-space 32 | --selfrequired 33 | --stripunusedargs closure-only 34 | --trailingclosures 35 | --wraparguments before-first 36 | --wrapcollections before-first 37 | --wrapparameters before-first 38 | 39 | # Rules 40 | --disable hoistAwait 41 | --disable hoistPatternLet 42 | --disable hoistTry 43 | --disable wrapMultilineStatementBraces 44 | --disable extensionAccessControl 45 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "swift", 6 | "args": [ 7 | "build", 8 | "--build-tests", 9 | "-Xlinker", 10 | "-debug:dwarf" 11 | ], 12 | "cwd": ".", 13 | "disableTaskQueue": true, 14 | "problemMatcher": [ 15 | "$swiftc" 16 | ], 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | }, 21 | "label": "swift: Build All", 22 | "detail": "swift build --build-tests -Xlinker -debug:dwarf" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | 3 | project(SwiftWebDriver 4 | LANGUAGES Swift) 5 | 6 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 7 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 9 | set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) 10 | 11 | option(SWIFT_WEBDRIVER_BUILD_WINAPPDRIVER "Build WinAppDriver functionality." TRUE) 12 | 13 | add_subdirectory(Sources/WebDriver) 14 | 15 | if(SWIFT_WEBDRIVER_BUILD_WINAPPDRIVER) 16 | add_subdirectory(Sources/WinAppDriver) 17 | endif() 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guidelines 2 | Welcome to swift-webdriver a Swift library for UI automation of Windows applications through the [WebDriver](https://w3c.github.io/webdriver/) protocol, similar to [Selenium](https://www.selenium.dev/), [Appium](http://appium.io/) or the [Windows Application Driver](https://github.com/microsoft/WinAppDriver). We welcome contributions, however before contributing, please read this entire document to ensure quality pull requests. 3 | 4 | ## Testing 5 | All UI tests are located in the `Tests` directory. Build and run tests using `swift build` and `swift test`, or use the [Swift extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang). 6 | 7 | For additional examples, refer to the `Tests\WebDriverTests` directory. 8 | 9 | ## Code Submissions 10 | Fork the repository and create a feature branch for your work. When 11 | ready, send a pull request. Make sure that the CI workflows that 12 | build and test the code pass. If you add files or dependencies, make 13 | sure you update both `Package.swift` and the appropriate `CMakeLists.txt` 14 | files. If you forget about CMake, your CI jobs will fail. `:-)` 15 | 16 | ## Bug Submissions 17 | If you've found a bug in the project feel free to open an issue [here](https://github.com/thebrowsercompany/swift-webdriver/issues/new). Please follow the issues template provided. 18 | 19 | ## Enhancement Requests 20 | To submit an enhancement open a new issue with the `enhancements` tag with details for what you think would be good to add. 21 | 22 | ## Style Guide 23 | For branch names please follow the naming convention of `/` e.g. squid/docs-update. When opening a pr use the most relevant tag to your request. 24 | 25 | ## 26 | Thank you for helping expand the swift environment and we look forward to working with you! 27 | -------------------------------------------------------------------------------- /Docs/SupportedAPIs.md: -------------------------------------------------------------------------------- 1 | ## Command Summary 2 | 3 | This table shows a mapping between WebDriver commands, backend support (currently just WinAppDriver), and the 4 | swift-webdriver API that implements the given command. 5 | 6 | Contributions to expand support to unimplemented functionality are always welcome. 7 | 8 | | Method | Command Path | WinAppDriver | swift-webdriver API | 9 | |--------|-----------------------------------------------------|--------------|---------------------| 10 | | GET | `/status` | Supported | `WebDriver.status` | 11 | | POST | `/session` | Supported | `Session.init()` | 12 | | GET | `/sessions` | Supported | Not implemented | 13 | | DELETE | `/session/:sessionId` | Supported | `Session.delete()`, `Session.deinit()`| 14 | | POST | `/session/:sessionId/appium/app/launch` | Supported | Not implemented | 15 | | POST | `/session/:sessionId/appium/app/close` | Supported | Not implemented | 16 | | POST | `/session/:sessionId/back` | Supported | `Session.back()` | 17 | | POST | `/session/:sessionId/buttondown` | Supported | `Session.buttonDown()`| 18 | | POST | `/session/:sessionId/buttonup` | Supported | `Session.buttonUp()`| 19 | | POST | `/session/:sessionId/click` | Supported | `Session.click()` | 20 | | POST | `/session/:sessionId/doubleclick` | Supported | `Session.doubleClick()`| 21 | | POST | `/session/:sessionId/element` | Supported | `Session.findElement()`| 22 | | POST | `/session/:sessionId/elements` | Supported | `Session.findElements()`| 23 | | POST | `/session/:sessionId/element/active` | Supported | `Session.activeElement`| 24 | | GET | `/session/:sessionId/element/:id/attribute/:name` | Supported | `Element.getAttribute`| 25 | | POST | `/session/:sessionId/element/:id/clear` | Supported | `Element.clear()` | 26 | | POST | `/session/:sessionId/element/:id/click` | Supported | `Element.click()` | 27 | | GET | `/session/:sessionId/element/:id/displayed` | Supported | `Element.displayed` | 28 | | GET | `/session/:sessionId/element/:id/element` | Supported | `Element.findElement()`| 29 | | GET | `/session/:sessionId/element/:id/elements` | Supported | `Element.findElements()`| 30 | | GET | `/session/:sessionId/element/:id/enabled` | Supported | `Element.enabled` | 31 | | GET | `/session/:sessionId/element/:id/equals` | Supported | Not implemented | 32 | | GET | `/session/:sessionId/element/:id/location` | Supported | `Element.location` | 33 | | GET | `/session/:sessionId/element/:id/location_in_view` | Supported | Not implemented | 34 | | GET | `/session/:sessionId/element/:id/name` | Supported | Not implemented | 35 | | GET | `/session/:sessionId/element/:id/screenshot` | Supported | Not implemented | 36 | | GET | `/session/:sessionId/element/:id/selected` | Supported | `Element.selected` | 37 | | GET | `/session/:sessionId/element/:id/size` | Supported | `Element.size` | 38 | | GET | `/session/:sessionId/element/:id/text` | Supported | `Element.text` | 39 | | POST | `/session/:sessionId/element/:id/value` | Supported | `Element.sendKeys()`| 40 | | POST | `/session/:sessionId/execute` | Not Supported| `Session.execute()` | 41 | | POST | `/session/:sessionId/execute_async` | Not Supported| `Session.execute()` | 42 | | POST | `/session/:sessionId/forward` | Supported | `Session.forward()` | 43 | | POST | `/session/:sessionId/keys` | Supported | `Session.sendKeys()`| 44 | | POST | `/session/:sessionId/location` | Supported | `Session.setLocation`| 45 | | GET | `/session/:sessionId/location` | Supported | `Session.location`| 46 | | POST | `/session/:sessionId/moveto` | Supported | `Session.moveTo()` | 47 | | GET | `/session/:sessionId/orientation` | Supported | `Session.orientation`| 48 | | POST | `/session/:sessionId/refresh` | Not supported| `Session.refresh()` | 49 | | GET | `/session/:sessionId/screenshot` | Supported | `Session.screenshot()`| 50 | | GET | `/session/:sessionId/source` | Supported | `Session.source` | 51 | | POST | `/session/:sessionId/timeouts` | Supported | `Session.setTimeout()`| 52 | | GET | `/session/:sessionId/title` | Supported | `Session.title` | 53 | | POST | `/session/:sessionId/touch/click` | Supported | `Element.touchClick()`| 54 | | POST | `/session/:sessionId/touch/doubleclick` | Supported | `Element.doubleClick()`| 55 | | POST | `/session/:sessionId/touch/down` | Supported | `Session.touchDown()`| 56 | | POST | `/session/:sessionId/touch/flick` | Supported | `Session.flick()`, `Element.flick()`| 57 | | POST | `/session/:sessionId/touch/longclick` | Supported | Not implemented | 58 | | POST | `/session/:sessionId/touch/move` | Supported | `Session.touchMove()`| 59 | | POST | `/session/:sessionId/touch/scroll` | Supported | `Session.touchScroll()`| 60 | | POST | `/session/:sessionId/touch/up` | Supported | `Session.touchUp()` | 61 | | GET | `/session/:sessionId/url` | Not supported| `Session.url` | 62 | | POST | `/session/:sessionId/url` | Not supported| `Session.url()` | 63 | | DELETE | `/session/:sessionId/window` | Supported | `Session.close()` | 64 | | POST | `/session/:sessionId/window` | Supported | `Session.focus()` | 65 | | POST | `/session/:sessionId/window/maximize` | Supported | Not implemented | 66 | | POST | `/session/:sessionId/window/size` | Supported | Not implemented | 67 | | GET | `/session/:sessionId/window/size` | Supported | Not implemented | 68 | | POST | `/session/:sessionId/window/:windowHandle/size` | Supported | `Window.setSize()` | 69 | | GET | `/session/:sessionId/window/:windowHandle/size` | Supported | `Window.size` | 70 | | POST | `/session/:sessionId/window/:windowHandle/position` | Supported | `Window.setPosition()`| 71 | | GET | `/session/:sessionId/window/:windowHandle/position` | Supported | `Window.position`| 72 | | POST | `/session/:sessionId/window/:windowHandle/maximize` | Supported | `Window.maximize()`| 73 | | GET | `/session/:sessionId/window_handle` | Supported | `Session.windowHandle`| 74 | | GET | `/session/:sessionId/window_handles` | Supported | `Session.windowHandles`| 75 | -------------------------------------------------------------------------------- /Docs/Usage.md: -------------------------------------------------------------------------------- 1 | ## Example Usage 2 | 3 | ### 1. Installing Swift 4 | Swift-WebDriver works with Swift 5.9 and onwards. If you haven't already, install the [most recent release of swift here](https://www.swift.org/install/windows/#installation-via-windows-package-manager). Verify installation with `swift -v` in the terminal 5 | 6 | ### 2. Initialize Directory 7 | Once you have verified the installation run the command in Windows Command Prompt: 8 | ```cmd 9 | mkdir swift-webdriver ^ 10 | cd swift-webdriver ^ 11 | swift package init --name swift-webdriver --type executable ^ 12 | ``` 13 | 14 | ### 3. Add Swift-WebDriver 15 | In your chosen code editor open the `Package.swift` file and add swift-webdriver to your packages. And example may look like this: 16 | ```swift 17 | let package = Package( 18 | name: "MyPackage", 19 | dependencies: [ 20 | .package(url: "https://github.com/thebrowsercompany/swift-webdriver", branch: "main") 21 | ], 22 | targets: [ 23 | .testTarget( 24 | name: "UITests", 25 | dependencies: [ 26 | .product(name: "WebDriver", package: "swift-webdriver"), 27 | ] 28 | ) 29 | ] 30 | ) 31 | ``` 32 | 33 | ### 4. Use Implemented Methods 34 | In the `main.swift` file add the following code: 35 | ```swift 36 | var session = Session() 37 | 38 | // Focus on the currently opened window 39 | session.focus("") 40 | 41 | // Scroll the window down 100px relative to the pointer position 42 | session.moveTo(nil, 0, -100) 43 | 44 | // Screenshot the current window 45 | session.screenshot() 46 | 47 | // Close the selected window 48 | session.close("") 49 | ``` 50 | Save the file and execute the command `swift run`. -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, The Browser Company 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-webdriver", 7 | products: [ 8 | .library(name: "WebDriver", targets: ["WebDriver"]), 9 | ] + ifWindows([ 10 | .library(name: "WinAppDriver", targets: ["WinAppDriver"]), 11 | ]), 12 | targets: [ 13 | .target( 14 | name: "WebDriver", 15 | path: "Sources/WebDriver", 16 | exclude: ["CMakeLists.txt"]), 17 | .target( 18 | name: "TestsCommon", 19 | path: "Tests/Common"), 20 | .testTarget( 21 | name: "UnitTests", 22 | dependencies: ["TestsCommon", "WebDriver"], 23 | // Ignore "LNK4217: locally defined symbol imported" spew due to SPM library support limitations 24 | linkerSettings: [ .unsafeFlags(["-Xlinker", "-ignore:4217"], .when(platforms: [.windows])) ]), 25 | .testTarget( 26 | name: "AppiumTests", 27 | dependencies: ["TestsCommon", "WebDriver"], 28 | // Ignore "LNK4217: locally defined symbol imported" spew due to SPM library support limitations 29 | linkerSettings: ifWindows([ .unsafeFlags(["-Xlinker", "-ignore:4217"]) ])), 30 | ] + ifWindows([ 31 | .target( 32 | name: "WinAppDriver", 33 | dependencies: ["WebDriver"], 34 | path: "Sources/WinAppDriver", 35 | exclude: ["CMakeLists.txt"]), 36 | .testTarget( 37 | name: "WinAppDriverTests", 38 | dependencies: ["TestsCommon", "WebDriver", "WinAppDriver"], 39 | // Ignore "LNK4217: locally defined symbol imported" spew due to SPM library support limitations 40 | linkerSettings: [ .unsafeFlags(["-Xlinker", "-ignore:4217"]) ]), 41 | ]) 42 | ) 43 | 44 | func ifWindows(_ values: [T]) -> [T] { 45 | #if os(Windows) 46 | return values 47 | #else 48 | return [] 49 | #endif 50 | } 51 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # swift-webdriver 2 | 3 | [![Build & Test](https://github.com/thebrowsercompany/swift-webdriver/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/thebrowsercompany/swift-webdriver/actions/workflows/build-and-test.yml) 4 | 5 | A Swift library for UI automation of apps and browsers via communication with [WebDriver](https://w3c.github.io/webdriver/) endpoints, such as [Selenium](https://www.selenium.dev/), [Appium](http://appium.io/) or the [Windows Application Driver](https://github.com/microsoft/WinAppDriver). 6 | 7 | `swift-webdriver` is meant to support both the [Selenium legacy JSON wire protocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/) and its successor, the W3C-standard [WebDriver protocol](https://w3c.github.io/webdriver/), against any WebDriver endpoint. In practice, it has been developed and tested for WinAppDriver-based scenarios on Windows, and may have gaps in other environments. 8 | 9 | ## Usage 10 | 11 | A `swift-webdriver` "Hello world" using `WinAppDriver` might look like this: 12 | 13 | ```swift 14 | let session = try Session( 15 | webDriver: WinAppDriver.start(), // Requires WinAppDriver to be installed on the machine 16 | desiredCapabilities: WinAppDriver.Capabilities.startApp(name: "notepad.exe")) 17 | try session.findElement(locator: .name("close")).click() 18 | ``` 19 | 20 | To use `swift-webdriver` in your project, add a reference to it in your `Package.swift` file as follows: 21 | 22 | ```swift 23 | let package = Package( 24 | name: "MyPackage", 25 | dependencies: [ 26 | .package(url: "https://github.com/thebrowsercompany/swift-webdriver", branch: "main") 27 | ], 28 | targets: [ 29 | .testTarget( 30 | name: "UITests", 31 | dependencies: [ 32 | .product(name: "WebDriver", package: "swift-webdriver"), 33 | ] 34 | ) 35 | ] 36 | ) 37 | ``` 38 | 39 | Build and run tests using `swift build` and `swift test`, or use the [Swift extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang). 40 | 41 | For additional examples, refer to the `Tests\WebDriverTests` directory. 42 | 43 | ### CMake 44 | 45 | To build with CMake, use the Ninja generator: 46 | ```powershell 47 | cmake -S . -B build -G Ninja 48 | cmake --build .\build\ 49 | ``` 50 | 51 | ## Architecture 52 | 53 | The library has two logical layers: 54 | 55 | - **Wire layer**: The `WebDriver` and `Request` protocols and their implementations provide a strongly-typed representation for sending REST requests to WebDriver endpoints. Each request is represented by a struct under `Requests`. The library can be used and extended only at this layer if desired. 56 | - **Session API layer**: The `Session` and `Element` types provide an object-oriented representation of WebDriver concepts with straightforward functions for every supported command such as `findElement(id:)` and `click()`. 57 | 58 | Where WebDriver endpoint-specific functionality is provided, such as for launching and using a WinAppDriver instance, the code is kept separate from generic WebDriver functionality as much as possible. 59 | 60 | ## Timeouts 61 | For UI testing, it is often useful to support retrying some operations until a timeout elapses (to account for animations or asynchrony). `swift-webdriver` offers two such mechanisms: 62 | 63 | - **Implicit wait timeout**: A duration for which `findElement` operations will implicitly wait if they cannot immediately find the element being queried. This feature is built into the WebDriver protocol, but optionally implemented by drivers. For drivers that do not support it, the library emulates it by repeating the query until the timeout elapses. By spec, this timeout defaults to zero. 64 | - **Interaction retry timeout**: A duration for which `click`, `flick` and similar operations will retry until they result in a successful interaction (e.g. the button is not disabled). This feature is not part of the WebDriver protocol, but rather implemented by the library. This timeout defaults to zero. 65 | 66 | ## Contributing 67 | 68 | We welcome contributions for: 69 | - Additional command bindings from the WebDriver and Selenium legacy JSON wire protocols 70 | - Improved support for other platforms and WebDriver endpoints 71 | 72 | Please include tests with your submissions. Tests launching apps should rely only on GUI applications that ship with the Windows OS, such as notepad. 73 | -------------------------------------------------------------------------------- /Sources/WebDriver/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(WebDriver 2 | Capabilities.swift 3 | Capabilities+AppiumOptions.swift 4 | Element.swift 5 | ElementLocator.swift 6 | ErrorResponse.swift 7 | HTTPWebDriver.swift 8 | Keys.swift 9 | Location.swift 10 | MouseButton.swift 11 | NoSuchElementError.swift 12 | Poll.swift 13 | Request.swift 14 | Requests.swift 15 | Requests+LegacySelenium.swift 16 | Requests+W3C.swift 17 | ScreenOrientation.swift 18 | Session.swift 19 | TimeoutType.swift 20 | TouchClickKind.swift 21 | URLRequestExtensions.swift 22 | WebDriver.swift 23 | WebDriverStatus.swift 24 | Window.swift 25 | WireProtocol.swift) 26 | -------------------------------------------------------------------------------- /Sources/WebDriver/Capabilities+AppiumOptions.swift: -------------------------------------------------------------------------------- 1 | extension Capabilities { 2 | /// Appium-specific capabilities. See https://appium.io/docs/en/2.0/guides/caps 3 | open class AppiumOptions: Codable { 4 | public var app: String? = nil 5 | public var appArguments: [String]? = nil 6 | public var appWorkingDir: String? = nil 7 | public var automationName: String? = nil 8 | public var deviceName: String? = nil 9 | public var eventTimings: Bool? = nil 10 | public var fullReset: Bool? = nil 11 | public var newCommandTimeout: Double? = nil 12 | public var noReset: Bool? = nil 13 | public var platformVersion: String? = nil 14 | public var printPageSourceOnFindFailure: Bool? = nil 15 | 16 | public init() {} 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case app 20 | case appArguments 21 | case appWorkingDir 22 | case automationName 23 | case deviceName 24 | case eventTimings 25 | case fullReset 26 | case newCommandTimeout 27 | case noReset 28 | case platformVersion 29 | case printPageSourceOnFindFailure 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Sources/WebDriver/Capabilities.swift: -------------------------------------------------------------------------------- 1 | open class Capabilities: Codable { 2 | // From https://www.w3.org/TR/webdriver1/#dfn-capability 3 | public var platformName: String? 4 | public var setWindowRect: Bool? 5 | public var timeouts: Timeouts? 6 | 7 | // From https://www.selenium.dev/documentation/legacy/json_wire_protocol/#capabilities-json-object 8 | public var takesScreenshot: Bool? 9 | public var nativeEvents: Bool? 10 | 11 | // From https://appium.io/docs/en/2.0/guides/caps 12 | public var appiumOptions: AppiumOptions? 13 | 14 | public init() {} 15 | 16 | // See https://www.w3.org/TR/webdriver1/#dfn-table-of-session-timeouts 17 | public struct Timeouts: Codable { 18 | public var script: Int? 19 | public var pageLoad: Int? 20 | public var implicit: Int? 21 | } 22 | 23 | private enum CodingKeys: String, CodingKey { 24 | case platformName 25 | case setWindowRect 26 | case timeouts 27 | 28 | case takesScreenshot 29 | case nativeEvents 30 | 31 | case appiumOptions = "appium:options" 32 | } 33 | } 34 | 35 | // Workaround to allow the WinAppDriver.Capabilities name, 36 | // where we can't resolve the global scope Capabilities using the module name, 37 | // as in WebDriver.Capabilities, because that resolves to the global scope protocol. 38 | public typealias BaseCapabilities = Capabilities -------------------------------------------------------------------------------- /Sources/WebDriver/Element.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.TimeInterval 2 | 3 | // Represents an element in the WebDriver protocol. 4 | public struct Element { 5 | var webDriver: WebDriver { session.webDriver } 6 | public let session: Session 7 | public let id: String 8 | 9 | public init(session: Session, id: String) { 10 | self.session = session 11 | self.id = id 12 | } 13 | 14 | /// The element's textual contents. 15 | public var text: String { 16 | get throws { 17 | try webDriver.send(Requests.ElementText( 18 | session: session.id, element: id)).value 19 | } 20 | } 21 | 22 | /// The x and y location of the element relative to the screen top left corner. 23 | public var location: (x: Int, y: Int) { 24 | get throws { 25 | let responseValue = try webDriver.send(Requests.ElementLocation( 26 | session: session.id, element: id)).value 27 | return (responseValue.x, responseValue.y) 28 | } 29 | } 30 | 31 | /// Gets the width and height of this element in pixels. 32 | public var size: (width: Int, height: Int) { 33 | get throws { 34 | let responseValue = try webDriver.send(Requests.ElementSize( 35 | session: session.id, element: id)).value 36 | return (responseValue.width, responseValue.height) 37 | } 38 | } 39 | 40 | /// Gets a value indicating whether this element is currently displayed. 41 | public var displayed: Bool { 42 | get throws { 43 | try webDriver.send(Requests.ElementDisplayed( 44 | session: session.id, element: id)).value 45 | } 46 | } 47 | 48 | /// Gets a value indicating whether this element is currently enabled. 49 | public var enabled: Bool { 50 | get throws { 51 | try webDriver.send(Requests.ElementEnabled( 52 | session: session.id, element: id)).value 53 | } 54 | } 55 | 56 | /// Gets a value indicating whether this element is currently selected. 57 | public var selected: Bool { 58 | get throws { 59 | try webDriver.send(Requests.ElementSelected( 60 | session: session.id, element: id)).value 61 | } 62 | } 63 | 64 | /// Clicks this element. 65 | public func click(retryTimeout: TimeInterval? = nil) throws { 66 | let request = Requests.ElementClick(session: session.id, element: id) 67 | try session.sendInteraction(request, retryTimeout: retryTimeout) 68 | } 69 | 70 | /// Clicks this element via touch. 71 | public func touchClick(kind: TouchClickKind = .single, retryTimeout: TimeInterval? = nil) throws { 72 | let request = Requests.SessionTouchClick(session: session.id, kind: kind, element: id) 73 | try session.sendInteraction(request, retryTimeout: retryTimeout) 74 | } 75 | 76 | /// Double clicks an element by id. 77 | public func doubleClick(retryTimeout: TimeInterval? = nil) throws { 78 | let request = Requests.SessionTouchDoubleClick(session: session.id, element: id) 79 | try session.sendInteraction(request, retryTimeout: retryTimeout) 80 | } 81 | 82 | /// - Parameters: 83 | /// - retryTimeout: The amount of time to retry the operation. Overrides the implicit interaction retry timeout. 84 | /// - element: Element id to click 85 | /// - xOffset: The x offset in pixels to flick by. 86 | /// - yOffset: The y offset in pixels to flick by. 87 | /// - speed: The speed in pixels per seconds. 88 | public func flick(xOffset: Double, yOffset: Double, speed: Double, retryTimeout: TimeInterval? = nil) throws { 89 | let request = Requests.SessionTouchFlickElement(session: session.id, element: id, xOffset: xOffset, yOffset: yOffset, speed: speed) 90 | try session.sendInteraction(request, retryTimeout: retryTimeout) 91 | } 92 | 93 | /// Search for an element using a given locator, starting from this element. 94 | /// - Parameter locator: The locator strategy to use. 95 | /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. 96 | /// - Returns: The element that was found, if any. 97 | @discardableResult // for use as an assertion 98 | public func findElement(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> Element { 99 | try session.findElement(startingAt: self, locator: locator, waitTimeout: waitTimeout) 100 | } 101 | 102 | /// Search for elements using a given locator, starting from this element. 103 | /// - Parameter using: The locator strategy to use. 104 | /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. 105 | /// - Returns: The elements that were found, or an empty array. 106 | public func findElements(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> [Element] { 107 | try session.findElements(startingAt: self, locator: locator, waitTimeout: waitTimeout) 108 | } 109 | 110 | /// Gets an attribute of this element. 111 | /// - Parameter name: the attribute name. 112 | /// - Returns: the attribute value string. 113 | public func getAttribute(name: String) throws -> String { 114 | try webDriver.send(Requests.ElementAttribute( 115 | session: session.id, element: id, attribute: name)).value 116 | } 117 | 118 | /// Sends key presses to this element. 119 | /// - Parameter keys: A key sequence according to the WebDriver spec. 120 | public func sendKeys(_ keys: Keys) throws { 121 | try webDriver.send(Requests.ElementValue( 122 | session: session.id, element: id, value: [keys.rawValue])) 123 | } 124 | 125 | /// Clears the text of an editable element. 126 | public func clear() throws { 127 | try webDriver.send(Requests.ElementClear( 128 | session: session.id, element: id)) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/WebDriver/ElementLocator.swift: -------------------------------------------------------------------------------- 1 | /// A locator strategy to use when searching for an element. 2 | public struct ElementLocator: Codable, Hashable { 3 | /// The locator strategy to use. 4 | public var using: String 5 | /// The search target. 6 | public var value: String 7 | 8 | public init(using: String, value: String) { 9 | self.using = using 10 | self.value = value 11 | } 12 | 13 | /// Matches an element whose class name contains the search value; compound class names are not permitted. 14 | public static func className(_ value: String) -> Self { 15 | Self(using: "class name", value: value) 16 | } 17 | 18 | /// Matches an element matching a CSS selector. 19 | public static func cssSelector(_ value: String) -> Self { 20 | Self(using: "css selector", value: value) 21 | } 22 | 23 | /// Matches an element whose ID attribute matches the search value. 24 | public static func id(_ value: String) -> Self { 25 | Self(using: "id", value: value) 26 | } 27 | 28 | /// Matches an element whose NAME attribute matches the search value. 29 | public static func name(_ value: String) -> Self { 30 | Self(using: "name", value: value) 31 | } 32 | 33 | /// Matches an anchor element whose visible text matches the search value. 34 | public static func linkText(_ value: String) -> Self { 35 | Self(using: "link text", value: value) 36 | } 37 | 38 | /// Returns an anchor element whose visible text partially matches the search value. 39 | public static func partialLinkText(_ value: String) -> Self { 40 | Self(using: "partial link text", value: value) 41 | } 42 | 43 | /// Returns an element whose tag name matches the search value. 44 | public static func tagName(_ value: String) -> Self { 45 | Self(using: "tag name", value: value) 46 | } 47 | 48 | /// Returns an element matching an XPath expression. 49 | public static func xpath(_ value: String) -> Self { 50 | Self(using: "xpath", value: value) 51 | } 52 | } -------------------------------------------------------------------------------- /Sources/WebDriver/ErrorResponse.swift: -------------------------------------------------------------------------------- 1 | /// A response received when an error occurs when processing a request. 2 | public struct ErrorResponse: Codable, Error, CustomStringConvertible { 3 | public var status: Status 4 | public var value: Value 5 | 6 | public var description: String { 7 | var str = "Error \(status.rawValue)" 8 | if let error = value.error { 9 | str += " (\(error))" 10 | } 11 | str += ": \(value.message)" 12 | return str 13 | } 14 | 15 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#response-status-codes 16 | public struct Status: Codable, Hashable, RawRepresentable { 17 | public var rawValue: Int 18 | 19 | public static let success = Self(rawValue: 0) 20 | public static let noSuchDriver = Self(rawValue: 6) 21 | public static let noSuchElement = Self(rawValue: 7) 22 | public static let noSuchFrame = Self(rawValue: 8) 23 | public static let unknownCommand = Self(rawValue: 9) 24 | public static let staleElementReference = Self(rawValue: 10) 25 | public static let elementNotVisible = Self(rawValue: 11) 26 | public static let invalidElementState = Self(rawValue: 12) 27 | public static let unknownError = Self(rawValue: 13) 28 | public static let elementIsNotSelectable = Self(rawValue: 15) 29 | public static let javaScriptError = Self(rawValue: 17) 30 | public static let xPathLookupError = Self(rawValue: 19) 31 | public static let timeout = Self(rawValue: 21) 32 | public static let noSuchWindow = Self(rawValue: 23) 33 | public static let invalidCookieDomain = Self(rawValue: 24) 34 | public static let unableToSetCookie = Self(rawValue: 25) 35 | public static let unexpectedAlertOpen = Self(rawValue: 26) 36 | public static let noAlertOpenError = Self(rawValue: 27) 37 | public static let scriptTimeout = Self(rawValue: 28) 38 | public static let invalidElementCoordinates = Self(rawValue: 29) 39 | public static let imeNotAvailable = Self(rawValue: 30) 40 | public static let imeEngineActivationFailed = Self(rawValue: 31) 41 | public static let invalidSelector = Self(rawValue: 32) 42 | public static let sessionNotCreatedException = Self(rawValue: 33) 43 | public static let moveTargetOutOfBounds = Self(rawValue: 34) 44 | 45 | public init(rawValue: Int) { 46 | self.rawValue = rawValue 47 | } 48 | } 49 | 50 | public struct Value: Codable { 51 | public var error: String? 52 | public var message: String 53 | public var stacktrace: String? 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/WebDriver/HTTPWebDriver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | /// Thrown if we fail to detect the protocol a webdriver server uses. 7 | public struct ProtocolDetectionError: Error {} 8 | 9 | /// A connection to a WebDriver server over HTTP. 10 | public struct HTTPWebDriver: WebDriver { 11 | private let serverURL: URL 12 | public let wireProtocol: WireProtocol 13 | 14 | public static let defaultRequestTimeout: TimeInterval = 5 // seconds 15 | 16 | public init(endpoint: URL, wireProtocol: WireProtocol) { 17 | serverURL = endpoint 18 | self.wireProtocol = wireProtocol 19 | } 20 | 21 | public static func createWithDetectedProtocol(serverURL: URL) throws -> HTTPWebDriver { 22 | .init(endpoint: serverURL, wireProtocol: try detectProtocol(serverURL: serverURL)) 23 | } 24 | 25 | public static func detectProtocol(serverURL: URL) throws -> WireProtocol { 26 | // The status request is the same for the Selenium Legacy JSON protocol and W3C, 27 | // but the response format is different. 28 | let urlRequest = try Self.buildURLRequest(serverURL: serverURL, Requests.LegacySelenium.Status()) 29 | 30 | // Send the request and decode result or error 31 | let (status, responseData) = try urlRequest.send() 32 | guard status == 200 else { 33 | throw try JSONDecoder().decode(ErrorResponse.self, from: responseData) 34 | } 35 | 36 | if let _ = try? JSONDecoder().decode(Requests.LegacySelenium.Status.Response.self, from: responseData) { 37 | return .legacySelenium 38 | } else if let _ = try? JSONDecoder().decode(Requests.W3C.Status.Response.self, from: responseData) { 39 | return .w3c 40 | } else { 41 | throw ProtocolDetectionError() 42 | } 43 | } 44 | 45 | @discardableResult 46 | public func send(_ request: Req) throws -> Req.Response { 47 | let urlRequest = try Self.buildURLRequest(serverURL: self.serverURL, request) 48 | 49 | // Send the request and decode result or error 50 | let (status, responseData) = try urlRequest.send() 51 | guard status == 200 else { 52 | throw try JSONDecoder().decode(ErrorResponse.self, from: responseData) 53 | } 54 | 55 | return try JSONDecoder().decode(Req.Response.self, from: responseData) 56 | } 57 | 58 | private static func buildURLRequest(serverURL: URL, _ request: Req) throws -> URLRequest { 59 | var url = serverURL 60 | for (index, pathComponent) in request.pathComponents.enumerated() { 61 | let last = index == request.pathComponents.count - 1 62 | url.appendPathComponent(pathComponent, isDirectory: !last) 63 | } 64 | 65 | var urlRequest = URLRequest(url: url) 66 | urlRequest.httpMethod = request.method.rawValue 67 | // TODO(#40): Setting timeoutInterval causes a crash when sending the request on the CI machines. 68 | // urlRequest.timeoutInterval = Self.defaultRequestTimeout 69 | 70 | // Add the body if the Request type defines one 71 | if Req.Body.self != CodableNone.self { 72 | urlRequest.addValue("application/json;charset=UTF-8", forHTTPHeaderField: "content-type") 73 | urlRequest.httpBody = try JSONEncoder().encode(request.body) 74 | } 75 | 76 | return urlRequest 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/WebDriver/Keys.swift: -------------------------------------------------------------------------------- 1 | /// Represents a sequence of WebDriver key events and characters. 2 | public struct Keys: RawRepresentable { 3 | /// A string encoding the key sequence as defined by the WebDriver spec. 4 | public var rawValue: String 5 | 6 | public init(rawValue: String) { self.rawValue = rawValue } 7 | 8 | /// Concatenates multiple key sequences into a single one. 9 | public static func sequence(_ keys: [Self]) -> Self { 10 | Self(rawValue: keys.reduce("") { $0 + $1.rawValue }) 11 | } 12 | 13 | /// Concatenates multiple key sequences into a single one. 14 | public static func sequence(_ keys: Self...) -> Self { 15 | sequence(keys) 16 | } 17 | } 18 | 19 | // MARK: Key constants 20 | extension Keys { 21 | public static let a = Self(rawValue: "a") 22 | public static let b = Self(rawValue: "b") 23 | public static let c = Self(rawValue: "c") 24 | public static let d = Self(rawValue: "d") 25 | public static let e = Self(rawValue: "e") 26 | public static let f = Self(rawValue: "f") 27 | public static let g = Self(rawValue: "g") 28 | public static let h = Self(rawValue: "h") 29 | public static let i = Self(rawValue: "i") 30 | public static let j = Self(rawValue: "j") 31 | public static let k = Self(rawValue: "k") 32 | public static let l = Self(rawValue: "l") 33 | public static let m = Self(rawValue: "m") 34 | public static let n = Self(rawValue: "n") 35 | public static let o = Self(rawValue: "o") 36 | public static let p = Self(rawValue: "p") 37 | public static let q = Self(rawValue: "q") 38 | public static let r = Self(rawValue: "r") 39 | public static let s = Self(rawValue: "s") 40 | public static let t = Self(rawValue: "t") 41 | public static let u = Self(rawValue: "u") 42 | public static let v = Self(rawValue: "v") 43 | public static let w = Self(rawValue: "w") 44 | public static let x = Self(rawValue: "x") 45 | public static let y = Self(rawValue: "y") 46 | public static let z = Self(rawValue: "z") 47 | 48 | public static let digit1 = Self(rawValue: "1") 49 | public static let digit2 = Self(rawValue: "2") 50 | public static let digit3 = Self(rawValue: "3") 51 | public static let digit4 = Self(rawValue: "4") 52 | public static let digit5 = Self(rawValue: "5") 53 | public static let digit6 = Self(rawValue: "6") 54 | public static let digit7 = Self(rawValue: "7") 55 | public static let digit8 = Self(rawValue: "8") 56 | public static let digit9 = Self(rawValue: "9") 57 | public static let digit0 = Self(rawValue: "0") 58 | 59 | public static let cancel = Self(rawValue: "\u{E001}") 60 | public static let help = Self(rawValue: "\u{E002}") 61 | public static let backspace = Self(rawValue: "\u{E003}") 62 | public static let tab = Self(rawValue: "\u{E004}") 63 | public static let clear = Self(rawValue: "\u{E005}") 64 | public static let returnKey = Self(rawValue: "\u{E006}") 65 | public static let enter = Self(rawValue: "\u{E007}") 66 | public static let pause = Self(rawValue: "\u{E00B}") 67 | public static let escape = Self(rawValue: "\u{E00C}") 68 | public static let space = Self(rawValue: "\u{E00D}") 69 | public static let pageup = Self(rawValue: "\u{E00E}") 70 | public static let pagedown = Self(rawValue: "\u{E00F}") 71 | public static let end = Self(rawValue: "\u{E010}") 72 | public static let home = Self(rawValue: "\u{E011}") 73 | public static let leftArrow = Self(rawValue: "\u{E012}") 74 | public static let upArrow = Self(rawValue: "\u{E013}") 75 | public static let rightArrow = Self(rawValue: "\u{E014}") 76 | public static let downArrow = Self(rawValue: "\u{E015}") 77 | public static let insert = Self(rawValue: "\u{E016}") 78 | public static let delete = Self(rawValue: "\u{E017}") 79 | public static let semicolon = Self(rawValue: "\u{E018}") 80 | public static let equals = Self(rawValue: "\u{E019}") 81 | public static let numpad0 = Self(rawValue: "\u{E01A}") 82 | public static let numpad1 = Self(rawValue: "\u{E01B}") 83 | public static let numpad2 = Self(rawValue: "\u{E01C}") 84 | public static let numpad3 = Self(rawValue: "\u{E01D}") 85 | public static let numpad4 = Self(rawValue: "\u{E01E}") 86 | public static let numpad5 = Self(rawValue: "\u{E01F}") 87 | public static let numpad6 = Self(rawValue: "\u{E020}") 88 | public static let numpad7 = Self(rawValue: "\u{E021}") 89 | public static let numpad8 = Self(rawValue: "\u{E022}") 90 | public static let numpad9 = Self(rawValue: "\u{E023}") 91 | public static let multiply = Self(rawValue: "\u{E024}") 92 | public static let add = Self(rawValue: "\u{E025}") 93 | public static let separator = Self(rawValue: "\u{E026}") 94 | public static let subtract = Self(rawValue: "\u{E027}") 95 | public static let decimal = Self(rawValue: "\u{E028}") 96 | public static let divide = Self(rawValue: "\u{E029}") 97 | public static let f1 = Self(rawValue: "\u{E031}") 98 | public static let f2 = Self(rawValue: "\u{E032}") 99 | public static let f3 = Self(rawValue: "\u{E033}") 100 | public static let f4 = Self(rawValue: "\u{E034}") 101 | public static let f5 = Self(rawValue: "\u{E035}") 102 | public static let f6 = Self(rawValue: "\u{E036}") 103 | public static let f7 = Self(rawValue: "\u{E037}") 104 | public static let f8 = Self(rawValue: "\u{E038}") 105 | public static let f9 = Self(rawValue: "\u{E039}") 106 | public static let f10 = Self(rawValue: "\u{E03A}") 107 | public static let f11 = Self(rawValue: "\u{E03B}") 108 | public static let f12 = Self(rawValue: "\u{E03C}") 109 | 110 | /// Modifier keys are interpreted as toggles instead of key presses. 111 | public static let shiftModifier = Keys(rawValue: "\u{E008}") 112 | public static let controlModifier = Keys(rawValue: "\u{E009}") 113 | public static let altModifier = Keys(rawValue: "\u{E00A}") 114 | public static let metaModifier = Keys(rawValue: "\u{E03D}") 115 | 116 | public static var windowsModifier: Keys { metaModifier } 117 | public static var macCommandModifier: Keys { metaModifier } 118 | public static var macOptionModifier: Keys { altModifier } 119 | 120 | /// A special Keys value that causes all modifiers to be released. 121 | public static let releaseModifiers = Keys(rawValue: "\u{E000}") 122 | } 123 | 124 | // MARK: Modifier sequences 125 | extension Keys { 126 | /// Wraps a keys sequence with holding and releasing the shift key. 127 | public static func shift(_ keys: Self) -> Self { 128 | sequence(shiftModifier, keys, shiftModifier) 129 | } 130 | 131 | /// Wraps a keys sequence with holding and releasing the control key. 132 | public static func control(_ keys: Self) -> Self { 133 | sequence(controlModifier, keys, controlModifier) 134 | } 135 | 136 | /// Wraps a keys sequence with holding and releasing the alt key. 137 | public static func alt(_ keys: Self) -> Self { 138 | sequence(altModifier, keys, altModifier) 139 | } 140 | 141 | /// Wraps a keys sequence with holding and releasing the meta key. 142 | public static func meta(_ keys: Self) -> Self { 143 | sequence(metaModifier, keys, metaModifier) 144 | } 145 | } 146 | 147 | // MARK: Text and typing 148 | extension Keys { 149 | public enum TypingStrategy { 150 | case assumeUSKeyboard 151 | case windowsKeyboardAgnostic 152 | } 153 | 154 | public static func text(_ str: String, typingStrategy: TypingStrategy) -> Self { 155 | switch typingStrategy { 156 | case .assumeUSKeyboard: return Self(rawValue: str) 157 | case .windowsKeyboardAgnostic: return text_windowsKeyboardAgnostic(str) 158 | } 159 | } 160 | 161 | private static func text_windowsKeyboardAgnostic(_ str: String) -> Self { 162 | var result = "" 163 | for codePoint in str.unicodeScalars { 164 | if isUSKeyboardKeyCharacter(codePoint) { 165 | // Avoid sending it as a key event, which is dependent on keyboard layout. 166 | // For example, the "q" key would type "a" on an AZERTY keyboard layout. 167 | // Instead, use the alt+numpad code to type the character. 168 | result += altModifier.rawValue 169 | for digit in String(codePoint.value) { 170 | switch digit { 171 | case "0": result += Self.numpad0.rawValue 172 | case "1": result += Self.numpad1.rawValue 173 | case "2": result += Self.numpad2.rawValue 174 | case "3": result += Self.numpad3.rawValue 175 | case "4": result += Self.numpad4.rawValue 176 | case "5": result += Self.numpad5.rawValue 177 | case "6": result += Self.numpad6.rawValue 178 | case "7": result += Self.numpad7.rawValue 179 | case "8": result += Self.numpad8.rawValue 180 | case "9": result += Self.numpad9.rawValue 181 | default: fatalError() 182 | } 183 | } 184 | result += altModifier.rawValue 185 | } 186 | else { 187 | // Other printable characters will be sent as character events, 188 | // independent of keyboard layout. 189 | result += String(codePoint) 190 | } 191 | } 192 | 193 | return Self(rawValue: result) 194 | } 195 | 196 | /// Tests whether a given character can be typed using a single key on a US English keyboard. 197 | /// The WebDriver spec will send these characters as key events, and expect them 198 | /// to be translated into the original character, but this depends on the keyboard layout. 199 | /// Characters like "A" and "!" are not listed because they require modifiers to type. 200 | private static func isUSKeyboardKeyCharacter(_ codePoint: UnicodeScalar) -> Bool { 201 | switch codePoint { 202 | case "a"..."z": return true 203 | case "0"..."9": return true 204 | case "`", "-", "=", "[", "]", "\\", ";", "'", ",", ".", "/": return true 205 | default: return false 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/WebDriver/Location.swift: -------------------------------------------------------------------------------- 1 | public struct Location: Codable, Equatable { 2 | public var latitude: Double 3 | public var longitude: Double 4 | public var altitude: Double 5 | 6 | public init(latitude: Double, longitude: Double, altitude: Double) { 7 | self.latitude = latitude 8 | self.longitude = longitude 9 | self.altitude = altitude 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Sources/WebDriver/MouseButton.swift: -------------------------------------------------------------------------------- 1 | /// Mouse button 2 | public enum MouseButton: Int, Codable { 3 | case left = 0 4 | case middle = 1 5 | case right = 2 6 | } 7 | -------------------------------------------------------------------------------- /Sources/WebDriver/NoSuchElementError.swift: -------------------------------------------------------------------------------- 1 | /// Thrown when findElement fails to locate an element. 2 | public struct NoSuchElementError: Error, CustomStringConvertible { 3 | /// The locator that was used to search for the element. 4 | public var locator: ElementLocator 5 | 6 | /// The error that caused the element to not be found. 7 | public var sourceError: Error 8 | 9 | public init(locator: ElementLocator, sourceError: Error) { 10 | self.locator = locator 11 | self.sourceError = sourceError 12 | } 13 | 14 | /// The error response returned by the WebDriver server, if this was the source of the failure. 15 | public var errorResponse: ErrorResponse? { sourceError as? ErrorResponse } 16 | 17 | public var description: String { 18 | "No element found using locator [\(locator.using)=\(locator.value)]: \(sourceError)" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/WebDriver/Poll.swift: -------------------------------------------------------------------------------- 1 | import class Foundation.Thread 2 | import struct Foundation.TimeInterval 3 | import struct Dispatch.DispatchTime 4 | 5 | /// Calls a closure repeatedly with exponential backoff until it reports success or a timeout elapses. 6 | /// Thrown errors bubble up immediately, returned errors allow retries. 7 | /// - Returns: The successful value. 8 | internal func poll( 9 | timeout: TimeInterval, 10 | initialPeriod: TimeInterval = 0.001, 11 | work: () throws -> Result) throws -> Value { 12 | let startTime = DispatchTime.now() 13 | var lastResult = try work() 14 | var period = initialPeriod 15 | while true { 16 | guard case .failure = lastResult else { break } 17 | 18 | // Check if we ran out of time and return the last result 19 | let elapsedTime = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000 20 | let remainingTime = timeout - elapsedTime 21 | if remainingTime < 0 { break } 22 | 23 | // Sleep for the next period and retry 24 | let sleepTime = min(period, remainingTime) 25 | Thread.sleep(forTimeInterval: sleepTime) 26 | 27 | lastResult = try work() 28 | period *= 2 // Exponential backoff 29 | } 30 | 31 | return try lastResult.get() 32 | } 33 | 34 | /// Calls a closure repeatedly with exponential backoff until it reports success or a timeout elapses. 35 | /// - Returns: Whether the closure reported success within the expected time. 36 | internal func poll( 37 | timeout: TimeInterval, 38 | initialPeriod: TimeInterval = 0.001, 39 | work: () throws -> Bool) throws -> Bool { 40 | struct FalseError: Error {} 41 | do { 42 | try poll(timeout: timeout, initialPeriod: initialPeriod) { 43 | try work() ? .success(()) : .failure(FalseError()) 44 | } 45 | return true 46 | } catch _ as FalseError { 47 | return false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/WebDriver/Request.swift: -------------------------------------------------------------------------------- 1 | public protocol Request { 2 | associatedtype Body: Codable = CodableNone 3 | associatedtype Response: Codable = CodableNone 4 | 5 | var pathComponents: [String] { get } 6 | var method: HTTPMethod { get } 7 | var body: Body { get } 8 | } 9 | 10 | extension Request where Body == CodableNone { 11 | public var body: Body { .init() } 12 | } 13 | 14 | public struct CodableNone: Codable {} 15 | 16 | public enum HTTPMethod: String { 17 | case get = "GET" 18 | case delete = "DELETE" 19 | case post = "POST" 20 | } 21 | -------------------------------------------------------------------------------- /Sources/WebDriver/Requests+LegacySelenium.swift: -------------------------------------------------------------------------------- 1 | extension Requests { 2 | /// Defines requests and response types specific to the legacy selenium json wire protocol. 3 | public enum LegacySelenium { 4 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#session 5 | public struct Session: Request { 6 | public var desiredCapabilities: Caps 7 | public var requiredCapabilities: Caps? 8 | 9 | public init(desiredCapabilities: Caps, requiredCapabilities: Caps? = nil) { 10 | self.requiredCapabilities = requiredCapabilities 11 | self.desiredCapabilities = desiredCapabilities 12 | } 13 | 14 | public var pathComponents: [String] { ["session"] } 15 | public var method: HTTPMethod { .post } 16 | public var body: Body { .init(desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities) } 17 | 18 | public struct Body: Codable { 19 | public var desiredCapabilities: Caps 20 | public var requiredCapabilities: Caps? 21 | } 22 | 23 | public struct Response: Codable { 24 | public var sessionId: String 25 | public var value: Caps 26 | } 27 | } 28 | 29 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#status 30 | public struct Status: Request { 31 | public var pathComponents: [String] { ["status"] } 32 | public var method: HTTPMethod { .get } 33 | 34 | public typealias Response = WebDriverStatus 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/WebDriver/Requests+W3C.swift: -------------------------------------------------------------------------------- 1 | extension Requests { 2 | /// Defines requests and response types specific to the W3C WebDriver protocol. 3 | public enum W3C { 4 | public struct Session: Request { 5 | public var alwaysMatch: Caps 6 | public var firstMatch: [Caps] 7 | 8 | public init(alwaysMatch: Caps, firstMatch: [Caps] = []) { 9 | self.alwaysMatch = alwaysMatch 10 | self.firstMatch = firstMatch 11 | } 12 | 13 | public var pathComponents: [String] { ["session"] } 14 | public var method: HTTPMethod { .post } 15 | public var body: Body { .init(capabilities: .init(alwaysMatch: alwaysMatch, firstMatch: firstMatch)) } 16 | 17 | public struct Body: Codable { 18 | public struct Capabilities: Codable { 19 | public var alwaysMatch: Caps 20 | public var firstMatch: [Caps]? 21 | } 22 | 23 | public var capabilities: Capabilities 24 | } 25 | 26 | public typealias Response = ResponseWithValue 27 | 28 | public struct ResponseValue: Codable { 29 | public var sessionId: String 30 | public var capabilities: Caps 31 | } 32 | } 33 | 34 | public struct Status: Request { 35 | public var pathComponents: [String] { ["status"] } 36 | public var method: HTTPMethod { .get } 37 | 38 | public typealias Response = ResponseWithValue 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/WebDriver/Requests.swift: -------------------------------------------------------------------------------- 1 | /// Defines request and response types for the WebDriver protocol. 2 | public enum Requests { 3 | public struct ResponseWithValue: Codable where Value: Codable { 4 | public var value: Value 5 | 6 | public init(_ value: Value) { 7 | self.value = value 8 | } 9 | 10 | internal enum CodingKeys: String, CodingKey { 11 | case value 12 | } 13 | } 14 | 15 | public struct ResponseWithValueArray: Codable where Value: Codable { 16 | public var value: [Value] 17 | 18 | public init(_ value: [Value]) { 19 | self.value = value 20 | } 21 | 22 | internal enum CodingKeys: String, CodingKey { 23 | case value 24 | } 25 | } 26 | 27 | public struct ElementResponseValue: Codable { 28 | public var element: String 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case element = "ELEMENT" 32 | } 33 | } 34 | 35 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidattributename 36 | public struct ElementAttribute: Request { 37 | public var session: String 38 | public var element: String 39 | public var attribute: String 40 | 41 | public var pathComponents: [String] { ["session", session, "element", element, "attribute", attribute] } 42 | public var method: HTTPMethod { .get } 43 | 44 | public typealias Response = ResponseWithValue 45 | } 46 | 47 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidclear 48 | public struct ElementClear: Request { 49 | public var session: String 50 | public var element: String 51 | 52 | public var pathComponents: [String] { ["session", session, "element", element, "clear"] } 53 | public var method: HTTPMethod { .post } 54 | } 55 | 56 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidclick 57 | public struct ElementClick: Request { 58 | public var session: String 59 | public var element: String 60 | 61 | public var pathComponents: [String] { ["session", session, "element", element, "click"] } 62 | public var method: HTTPMethod { .post } 63 | } 64 | 65 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementiddisplayed 66 | public struct ElementDisplayed: Request { 67 | public var session: String 68 | public var element: String 69 | 70 | public var pathComponents: [String] { ["session", session, "element", element, "displayed"] } 71 | public var method: HTTPMethod { .get } 72 | 73 | // Override the whole Response struct instead of just ResponseValue 74 | // because the value is a Bool, which does not conform to Codable. 75 | public struct Response: Codable { 76 | public var value: Bool 77 | } 78 | } 79 | 80 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidenabled 81 | public struct ElementEnabled: Request { 82 | public var session: String 83 | public var element: String 84 | 85 | public var pathComponents: [String] { ["session", session, "element", element, "enabled"] } 86 | public var method: HTTPMethod { .get } 87 | 88 | // Override the whole Response struct instead of just ResponseValue 89 | // because the value is a Bool, which does not conform to Codable. 90 | public struct Response: Codable { 91 | public var value: Bool 92 | } 93 | } 94 | 95 | public struct ElementSelected: Request { 96 | public var session: String 97 | public var element: String 98 | 99 | public var pathComponents: [String] { ["session", session, "element", element, "selected"] } 100 | public var method: HTTPMethod { .get } 101 | 102 | public struct Response: Codable { 103 | public var value: Bool 104 | } 105 | } 106 | 107 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidvalue 108 | public struct ElementValue: Request { 109 | public var session: String 110 | public var element: String 111 | public var value: [String] 112 | 113 | public var pathComponents: [String] { ["session", session, "element", element, "value"] } 114 | 115 | public var method: HTTPMethod { .post } 116 | public var body: Body { .init(value: value) } 117 | 118 | public struct Body: Codable { 119 | public var value: [String] 120 | } 121 | } 122 | 123 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidlocation 124 | public struct ElementLocation: Request { 125 | public var session: String 126 | public var element: String 127 | 128 | public var pathComponents: [String] { ["session", session, "element", element, "location"] } 129 | public var method: HTTPMethod { .get } 130 | 131 | public typealias Response = ResponseWithValue 132 | public struct ResponseValue: Codable { 133 | public var x: Int 134 | public var y: Int 135 | } 136 | } 137 | 138 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidsize 139 | public struct ElementSize: Request { 140 | public var session: String 141 | public var element: String 142 | 143 | public var pathComponents: [String] { ["session", session, "element", element, "size"] } 144 | public var method: HTTPMethod { .get } 145 | 146 | public typealias Response = ResponseWithValue 147 | public struct ResponseValue: Codable { 148 | public var width: Int 149 | public var height: Int 150 | } 151 | } 152 | 153 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidtext 154 | public struct ElementText: Request { 155 | public var session: String 156 | public var element: String 157 | 158 | public var pathComponents: [String] { ["session", session, "element", element, "text"] } 159 | public var method: HTTPMethod { .get } 160 | 161 | public typealias Response = ResponseWithValue 162 | } 163 | 164 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementactive 165 | public struct SessionActiveElement: Request { 166 | public var session: String 167 | 168 | public var pathComponents: [String] { ["session", session, "element", "active"] } 169 | public var method: HTTPMethod { .post } 170 | 171 | public typealias Response = ResponseWithValue 172 | } 173 | 174 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidback 175 | public struct SessionBack: Request { 176 | public var session: String 177 | 178 | public var pathComponents: [String] { ["session", session, "back"] } 179 | public var method: HTTPMethod { .post } 180 | } 181 | 182 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidbuttondown 183 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidbuttonup 184 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidclick 185 | public struct SessionButton: Request { 186 | public var session: String 187 | public var action: Action 188 | public var button: MouseButton 189 | 190 | public var pathComponents: [String] { ["session", session, action.rawValue] } 191 | public var method: HTTPMethod { .post } 192 | public var body: Body { .init(button: button) } 193 | 194 | public struct Body: Codable { 195 | public var button: MouseButton 196 | } 197 | 198 | public enum Action: String { 199 | case click 200 | case buttonUp = "buttonup" 201 | case buttonDown = "buttondown" 202 | } 203 | } 204 | 205 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionid 206 | public struct SessionDelete: Request { 207 | public var session: String 208 | 209 | public var pathComponents: [String] { ["session", session] } 210 | public var method: HTTPMethod { .delete } 211 | } 212 | 213 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessioniddoubleclick 214 | public struct SessionDoubleClick: Request { 215 | public var session: String 216 | 217 | public var pathComponents: [String] { ["session", session, "doubleclick"] } 218 | public var method: HTTPMethod { .post } 219 | } 220 | 221 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelement 222 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidelement 223 | public struct SessionElement: Request { 224 | public var session: String 225 | public var element: String? = nil 226 | public var locator: ElementLocator 227 | 228 | public var pathComponents: [String] { 229 | if let element { 230 | return ["session", session, "element", element, "element"] 231 | } else { 232 | return ["session", session, "element"] 233 | } 234 | } 235 | 236 | public var method: HTTPMethod { .post } 237 | public var body: ElementLocator { locator } 238 | 239 | public typealias Response = ResponseWithValue 240 | } 241 | 242 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelements 243 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidelementidelements 244 | public struct SessionElements: Request { 245 | public var session: String 246 | public var element: String? = nil 247 | public var locator: ElementLocator 248 | 249 | public var pathComponents: [String] { 250 | if let element { 251 | return ["session", session, "element", element, "elements"] 252 | } else { 253 | return ["session", session, "elements"] 254 | } 255 | } 256 | 257 | public var method: HTTPMethod { .post } 258 | public var body: ElementLocator { locator } 259 | 260 | public typealias Response = ResponseWithValueArray 261 | } 262 | 263 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidforward 264 | public struct SessionForward: Request { 265 | public var session: String 266 | 267 | public var pathComponents: [String] { ["session", session, "forward"] } 268 | public var method: HTTPMethod { .post } 269 | } 270 | 271 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidkeys 272 | public struct SessionKeys: Request { 273 | public var session: String 274 | public var value: [String] 275 | 276 | public var pathComponents: [String] { ["session", session, "keys"] } 277 | public var method: HTTPMethod { .post } 278 | public var body: Body { .init(value: value) } 279 | 280 | public struct Body: Codable { 281 | public var value: [String] 282 | } 283 | } 284 | 285 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidmoveto 286 | public struct SessionMoveTo: Request { 287 | public var session: String 288 | public var element: String? 289 | public var xOffset: Int 290 | public var yOffset: Int 291 | 292 | public var pathComponents: [String] { ["session", session, "moveto"] } 293 | public var method: HTTPMethod { .post } 294 | public var body: Body { .init(element: element, xOffset: xOffset, yOffset: yOffset) } 295 | 296 | public struct Body: Codable { 297 | public var element: String? 298 | public var xOffset: Int 299 | public var yOffset: Int 300 | 301 | enum CodingKeys: String, CodingKey { 302 | case element = "element" 303 | case xOffset = "xoffset" 304 | case yOffset = "yoffset" 305 | } 306 | } 307 | } 308 | 309 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidrefresh 310 | public struct SessionRefresh: Request { 311 | public var session: String 312 | 313 | public var pathComponents: [String] { ["session", session, "refresh"] } 314 | public var method: HTTPMethod { .post } 315 | } 316 | 317 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidscreenshot 318 | public struct SessionScreenshot: Request { 319 | public var session: String 320 | 321 | public var pathComponents: [String] { ["session", session, "screenshot"] } 322 | public var method: HTTPMethod { .get } 323 | 324 | public typealias Response = ResponseWithValue 325 | } 326 | 327 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtimeouts 328 | public struct SessionTimeouts: Request { 329 | public var session: String 330 | public var type: TimeoutType 331 | public var ms: Double 332 | 333 | public var pathComponents: [String] { ["session", session, "timeouts"] } 334 | public var method: HTTPMethod { .post } 335 | public var body: Body { .init(type: type, ms: ms) } 336 | 337 | public struct Body: Codable { 338 | public var type: TimeoutType 339 | public var ms: Double 340 | } 341 | } 342 | 343 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtitle 344 | public struct SessionTitle: Request { 345 | public var session: String 346 | 347 | public var pathComponents: [String] { ["session", session, "title"] } 348 | public var method: HTTPMethod { .get } 349 | 350 | public typealias Response = ResponseWithValue 351 | } 352 | 353 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchmove 354 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchdown 355 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchup 356 | public struct SessionTouchAt: Request { 357 | public var session: String 358 | public var action: Action 359 | public var x: Int 360 | public var y: Int 361 | 362 | public var pathComponents: [String] { ["session", session, "touch", action.rawValue] } 363 | public var method: HTTPMethod { .post } 364 | public var body: Body { .init(x: x, y: y) } 365 | 366 | public struct Body: Codable { 367 | public var x: Int 368 | public var y: Int 369 | } 370 | 371 | public enum Action: String, Codable { 372 | case move 373 | case down 374 | case up 375 | } 376 | } 377 | 378 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchclick 379 | public struct SessionTouchClick: Request { 380 | public var session: String 381 | public var kind: TouchClickKind 382 | public var element: String 383 | 384 | public var pathComponents: [String] { ["session", session, "touch", kind.rawValue] } 385 | public var method: HTTPMethod { .post } 386 | public var body: Body { .init(element: element) } 387 | 388 | public struct Body: Codable { 389 | public var element: String 390 | } 391 | } 392 | 393 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchscroll 394 | public struct SessionTouchScroll: Request { 395 | public var session: String 396 | public var element: String? 397 | public var xOffset: Int 398 | public var yOffset: Int 399 | 400 | public var pathComponents: [String] { ["session", session, "touch", "scroll"] } 401 | public var method: HTTPMethod { .post } 402 | public var body: Body { .init(element: element, xOffset: xOffset, yOffset: yOffset) } 403 | 404 | public struct Body: Codable { 405 | public var element: String? 406 | public var xOffset: Int 407 | public var yOffset: Int 408 | 409 | private enum CodingKeys: String, CodingKey { 410 | case element = "element" 411 | case xOffset = "xoffset" 412 | case yOffset = "yoffset" 413 | } 414 | } 415 | } 416 | 417 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidurl 418 | public enum SessionUrl { 419 | public struct Get: Request { 420 | public var session: String 421 | 422 | public var pathComponents: [String] { ["session", session, "url"] } 423 | public var method: HTTPMethod { .get } 424 | 425 | public typealias Response = ResponseWithValue 426 | } 427 | 428 | public struct Post: Request { 429 | public var session: String 430 | public var url: String 431 | 432 | public var pathComponents: [String] { ["session", session, "url"] } 433 | public var method: HTTPMethod { .post } 434 | public var body: Body { .init(url: url) } 435 | 436 | public struct Body: Codable { 437 | public var url: String 438 | } 439 | } 440 | } 441 | 442 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidexecute 443 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidexecute_async 444 | public struct SessionScript: Request { 445 | public var session: String 446 | public var script: String 447 | public var args: [String] 448 | public var async: Bool 449 | 450 | public var pathComponents: [String] { ["session", session, async ? "execute_async" : "execute"] } 451 | public var method: HTTPMethod { .post } 452 | public var body: Body { .init(script: script, args: args) } 453 | public struct Body: Codable { 454 | public var script: String 455 | public var args: [String] 456 | } 457 | } 458 | 459 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidwindow 460 | public enum SessionWindow { 461 | public struct Post: Request { 462 | public var session: String 463 | public var name: String 464 | 465 | public var pathComponents: [String] { ["session", session, "window"] } 466 | public var method: HTTPMethod { .post } 467 | public var body: Body { .init(name: name) } 468 | 469 | public struct Body: Codable { 470 | public var name: String 471 | } 472 | } 473 | 474 | public struct Delete: Request { 475 | public var session: String 476 | public var name: String 477 | 478 | public var pathComponents: [String] { ["session", session, "window"] } 479 | public var method: HTTPMethod { .delete } 480 | public var body: Body { .init(name: name) } 481 | 482 | public struct Body: Codable { 483 | public var name: String 484 | } 485 | } 486 | } 487 | 488 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidwindowwindowhandlesize 489 | public enum WindowSize { 490 | public struct Post: Request { 491 | public var session: String 492 | public var windowHandle: String 493 | public var width: Double 494 | public var height: Double 495 | 496 | public var pathComponents: [String] { ["session", session, "window", windowHandle, "size"] } 497 | public var method: HTTPMethod { .post } 498 | public var body: Body { .init(width: width, height: height) } 499 | 500 | public struct Body: Codable { 501 | public var width: Double 502 | public var height: Double 503 | } 504 | } 505 | 506 | public struct Get: Request { 507 | public var session: String 508 | public var windowHandle: String 509 | 510 | public var pathComponents: [String] { ["session", session, "window", windowHandle, "size"] } 511 | public var method: HTTPMethod { .get } 512 | 513 | public typealias Response = ResponseWithValue 514 | public struct ResponseValue: Codable { 515 | public var width: Double 516 | public var height: Double 517 | } 518 | } 519 | } 520 | 521 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchdoubleclick 522 | public struct SessionTouchDoubleClick: Request { 523 | public var session: String 524 | public var element: String 525 | 526 | public var pathComponents: [String] { ["session", session, "touch", "doubleclick"] } 527 | public var method: HTTPMethod { .post } 528 | public var body: Body { .init(element: element) } 529 | 530 | public struct Body: Codable { 531 | public var element: String 532 | } 533 | } 534 | 535 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchflick 536 | public struct SessionTouchFlickElement: Request { 537 | public var session: String 538 | public var element: String 539 | public var xOffset: Double 540 | public var yOffset: Double 541 | public var speed: Double 542 | 543 | public var pathComponents: [String] { ["session", session, "touch", "flick"] } 544 | public var method: HTTPMethod { .post } 545 | public var body: Body { .init(xOffset: xOffset, yOffset: yOffset, speed: speed) } 546 | 547 | public struct Body: Codable { 548 | public var xOffset: Double 549 | public var yOffset: Double 550 | public var speed: Double 551 | 552 | private enum CodingKeys: String, CodingKey { 553 | case xOffset = "xoffset" 554 | case yOffset = "yoffset" 555 | case speed = "speed" 556 | } 557 | } 558 | } 559 | 560 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidtouchflick-1 561 | public struct SessionTouchFlick: Request { 562 | public var session: String 563 | public var xSpeed: Double 564 | public var ySpeed: Double 565 | 566 | public var pathComponents: [String] { ["session", session, "touch", "flick"] } 567 | public var method: HTTPMethod { .post } 568 | public var body: Body { .init(xSpeed: xSpeed, ySpeed: ySpeed) } 569 | 570 | public struct Body: Codable { 571 | public var xSpeed: Double 572 | public var ySpeed: Double 573 | 574 | private enum CodingKeys: String, CodingKey { 575 | case xSpeed = "xspeed" 576 | case ySpeed = "yspeed" 577 | } 578 | } 579 | } 580 | 581 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidlocation 582 | public enum SessionLocation { 583 | public struct Post: Request { 584 | public var session: String 585 | public var location: Location 586 | 587 | public var pathComponents: [String] { ["session", session, "location"] } 588 | public var method: HTTPMethod { .post } 589 | public var body: Location { location } 590 | } 591 | 592 | public struct Get: Request { 593 | public var session: String 594 | 595 | public var pathComponents: [String] { ["session", session, "location"] } 596 | public var method: HTTPMethod {.get} 597 | 598 | public typealias Response = ResponseWithValue 599 | } 600 | } 601 | 602 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidsource 603 | public struct SessionSource: Request { 604 | public var session: String 605 | 606 | public var pathComponents: [String] { ["session", session, "source"] } 607 | public var method: HTTPMethod { .get } 608 | 609 | public typealias Response = ResponseWithValue 610 | } 611 | 612 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidorientation 613 | public enum SessionOrientation { 614 | public struct Post: Request { 615 | public var session: String 616 | public var orientation: ScreenOrientation 617 | 618 | public var pathComponents: [String] { ["session", session, "orientation"] } 619 | public var method: HTTPMethod { .post } 620 | public var body: Body { .init(orientation: orientation) } 621 | 622 | public struct Body: Codable { 623 | public var orientation: ScreenOrientation 624 | } 625 | } 626 | 627 | public struct Get: Request { 628 | public var session: String 629 | 630 | public var pathComponents: [String] { ["session", session, "orientation"] } 631 | public var method: HTTPMethod { .get } 632 | 633 | public typealias Response = ResponseWithValue 634 | } 635 | } 636 | 637 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidwindowwindowhandleposition 638 | public enum WindowPosition { 639 | public struct Post: Request { 640 | public var session: String 641 | public var windowHandle: String 642 | public var x: Double 643 | public var y: Double 644 | 645 | public var pathComponents: [String] { ["session", session, "window", windowHandle, "position"] } 646 | public var method: HTTPMethod { .post } 647 | public var body: Body { .init(x: x, y: y) } 648 | 649 | public struct Body: Codable { 650 | public var x: Double 651 | public var y: Double 652 | } 653 | } 654 | 655 | public struct Get: Request { 656 | public var session: String 657 | public var windowHandle: String 658 | 659 | public var pathComponents: [String] { ["session", session, "window", windowHandle, "position"] } 660 | public var method: HTTPMethod { .get } 661 | 662 | public typealias Response = ResponseWithValue 663 | public struct ResponseValue: Codable { 664 | public var x: Double 665 | public var y: Double 666 | } 667 | } 668 | } 669 | 670 | public struct WindowMaximize: Request { 671 | public var session: String 672 | public var windowHandle: String 673 | 674 | public var pathComponents: [String] { ["session", session, "window", windowHandle, "maximize"] } 675 | public var method: HTTPMethod { .post } 676 | } 677 | 678 | // https://www.selenium.dev/documentation/legacy/json_wire_protocol/#sessionsessionidwindow_handle 679 | public struct SessionWindowHandle: Request { 680 | public var session: String 681 | 682 | public var pathComponents: [String] { ["session", session, "window_handle"] } 683 | public var method: HTTPMethod { .get } 684 | 685 | public typealias Response = ResponseWithValue 686 | } 687 | 688 | public struct SessionWindowHandles: Request { 689 | public var session: String 690 | 691 | public var pathComponents: [String] { ["session", session, "window_handles"] } 692 | public var method: HTTPMethod { .get } 693 | 694 | public typealias Response = ResponseWithValue> 695 | } 696 | } 697 | -------------------------------------------------------------------------------- /Sources/WebDriver/ScreenOrientation.swift: -------------------------------------------------------------------------------- 1 | // Screen orientations 2 | public enum ScreenOrientation: String, Codable { 3 | case portrait = "PORTRAIT" 4 | case landscape = "LANDSCAPE" 5 | } -------------------------------------------------------------------------------- /Sources/WebDriver/Session.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a session in the WebDriver protocol, 4 | /// which manages the lifetime of a page or app under UI automation. 5 | public final class Session { 6 | public let webDriver: any WebDriver 7 | public let id: String 8 | public let capabilities: Capabilities 9 | private var _implicitWaitTimeout: TimeInterval = 0 10 | internal var emulateImplicitWait: Bool = false // Set if the session doesn't support implicit waits. 11 | private var shouldDelete: Bool = true 12 | 13 | public init(webDriver: any WebDriver, existingId: String, capabilities: Capabilities = Capabilities(), owned: Bool = false) { 14 | self.webDriver = webDriver 15 | self.id = existingId 16 | self.capabilities = capabilities 17 | if let implicitWaitTimeoutInMilliseconds = capabilities.timeouts?.implicit { 18 | self.implicitWaitTimeout = Double(implicitWaitTimeoutInMilliseconds) / 1000.0 19 | } 20 | self.shouldDelete = owned 21 | } 22 | 23 | /// Initializer for Legacy Selenium JSON Protocol 24 | fileprivate convenience init(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities?) throws { 25 | let response = try webDriver.send(Requests.LegacySelenium.Session( 26 | desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities)) 27 | self.init( 28 | webDriver: webDriver, 29 | existingId: response.sessionId, 30 | capabilities: response.value, 31 | owned: true) 32 | } 33 | 34 | /// Initializer for W3C Protocol 35 | fileprivate convenience init(webDriver: any WebDriver, alwaysMatch: Capabilities, firstMatch: [Capabilities]) throws { 36 | let response = try webDriver.send(Requests.W3C.Session( 37 | alwaysMatch: alwaysMatch, firstMatch: firstMatch)) 38 | self.init( 39 | webDriver: webDriver, 40 | existingId: response.value.sessionId, 41 | capabilities: response.value.capabilities, 42 | owned: true) 43 | } 44 | 45 | public convenience init(webDriver: any WebDriver, capabilities: Capabilities) throws { 46 | switch webDriver.wireProtocol { 47 | case .legacySelenium: 48 | try self.init(webDriver: webDriver, desiredCapabilities: capabilities, requiredCapabilities: capabilities) 49 | case .w3c: 50 | try self.init(webDriver: webDriver, alwaysMatch: capabilities, firstMatch: []) 51 | } 52 | } 53 | 54 | public enum LegacySelenium { 55 | public static func create(webDriver: any WebDriver, desiredCapabilities: Capabilities, requiredCapabilities: Capabilities? = nil) throws -> Session { 56 | try Session(webDriver: webDriver, desiredCapabilities: desiredCapabilities, requiredCapabilities: requiredCapabilities) 57 | } 58 | } 59 | 60 | public enum W3C { 61 | public static func create(webDriver: any WebDriver, alwaysMatch: Capabilities, firstMatch: [Capabilities] = []) throws -> Session { 62 | try Session(webDriver: webDriver, alwaysMatch: alwaysMatch, firstMatch: firstMatch) 63 | } 64 | } 65 | 66 | /// The amount of time the driver should implicitly wait when searching for elements. 67 | /// This functionality is either implemented by the driver, or emulated by swift-webdriver as a fallback. 68 | public var implicitWaitTimeout: TimeInterval { 69 | get { _implicitWaitTimeout } 70 | set { 71 | if newValue == _implicitWaitTimeout { return } 72 | if !emulateImplicitWait { 73 | do { 74 | try setTimeout(type: .implicitWait, duration: newValue) 75 | } catch { 76 | emulateImplicitWait = true 77 | } 78 | } 79 | _implicitWaitTimeout = newValue 80 | } 81 | } 82 | 83 | /// The amount of time interactions should be retried before failing. 84 | /// This functionality is emulated by swift-webdriver. 85 | public var implicitInteractionRetryTimeout: TimeInterval = .zero 86 | 87 | /// The title of this session such as the tab or window text. 88 | public var title: String { 89 | get throws { 90 | try webDriver.send(Requests.SessionTitle(session: id)).value 91 | } 92 | } 93 | 94 | /// The current URL of this session. 95 | public var url: URL { 96 | get throws { 97 | guard let result = URL(string: try webDriver.send(Requests.SessionUrl.Get(session: id)).value) else { 98 | throw DecodingError.dataCorrupted( 99 | DecodingError.Context( 100 | codingPath: [Requests.SessionUrl.Get.Response.CodingKeys.value], 101 | debugDescription: "Invalid url format.")) 102 | } 103 | return result 104 | } 105 | } 106 | 107 | /// Navigates to a given URL. 108 | /// This is logically a setter for the 'url' property, 109 | /// but Swift doesn't support throwing setters. 110 | public func url(_ url: URL) throws { 111 | try webDriver.send(Requests.SessionUrl.Post(session: id, url: url.absoluteString)) 112 | } 113 | 114 | /// The active (focused) element. 115 | public var activeElement: Element? { 116 | get throws { 117 | do { 118 | let response = try webDriver.send(Requests.SessionActiveElement(session: id)) 119 | return Element(session: self, id: response.value.element) 120 | } catch let error as ErrorResponse where error.status == .noSuchElement { 121 | return nil 122 | } 123 | } 124 | } 125 | 126 | public var location: Location { 127 | get throws { 128 | let response = try webDriver.send(Requests.SessionLocation.Get(session: id)) 129 | return response.value 130 | } 131 | } 132 | 133 | public var orientation: ScreenOrientation { 134 | get throws { 135 | let response = try webDriver.send(Requests.SessionOrientation.Get(session: id)) 136 | return response.value 137 | } 138 | } 139 | 140 | /// Sets a a timeout value on this session. 141 | public func setTimeout(type: TimeoutType, duration: TimeInterval) throws { 142 | try webDriver.send( 143 | Requests.SessionTimeouts(session: id, type: type, ms: duration * 1000)) 144 | // Keep track of the implicit wait to know when we need to override it. 145 | if type == .implicitWait { _implicitWaitTimeout = duration } 146 | } 147 | 148 | public func execute(script: String, args: [String] = [], async: Bool = false) throws { 149 | try webDriver.send(Requests.SessionScript(session: id, script: script, args: args, async: async)) 150 | } 151 | 152 | public func back() throws { 153 | try webDriver.send(Requests.SessionBack(session: id)) 154 | } 155 | 156 | public func forward() throws { 157 | try webDriver.send(Requests.SessionForward(session: id)) 158 | } 159 | 160 | public func refresh() throws { 161 | try webDriver.send(Requests.SessionRefresh(session: id)) 162 | } 163 | 164 | /// Takes a screenshot of the current page. 165 | /// - Returns: The screenshot data as a PNG file. 166 | public func screenshot() throws -> Data { 167 | let base64: String = try webDriver.send( 168 | Requests.SessionScreenshot(session: id)).value 169 | guard let data = Data(base64Encoded: base64) else { 170 | let codingPath = [Requests.SessionScreenshot.Response.CodingKeys.value] 171 | let description = "Invalid Base64 string while decoding screenshot response." 172 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: description)) 173 | } 174 | return data 175 | } 176 | 177 | /// Finds an element using a given locator, starting from the session root. 178 | /// - Parameter locator: The locator strategy to use. 179 | /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. 180 | /// - Returns: The element that was found, if any. 181 | @discardableResult // for use as an assertion 182 | public func findElement(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> Element { 183 | try findElement(startingAt: nil, locator: locator, waitTimeout: waitTimeout) 184 | } 185 | 186 | /// Finds elements by id, starting from the root. 187 | /// - Parameter locator: The locator strategy to use. 188 | /// - Parameter waitTimeout: The amount of time to wait for element existence. Overrides the implicit wait timeout. 189 | /// - Returns: The elements that were found, or an empty array. 190 | public func findElements(locator: ElementLocator, waitTimeout: TimeInterval? = nil) throws -> [Element] { 191 | try findElements(startingAt: nil, locator: locator, waitTimeout: waitTimeout) 192 | } 193 | 194 | /// Overrides the implicit wait timeout during a block of code. 195 | private func withImplicitWaitTimeout(_ value: TimeInterval?, _ block: () throws -> Result) rethrows -> Result { 196 | if let value, value != _implicitWaitTimeout { 197 | let previousValue = _implicitWaitTimeout 198 | implicitWaitTimeout = value 199 | defer { implicitWaitTimeout = previousValue } 200 | return try block() 201 | } 202 | else { 203 | return try block() 204 | } 205 | } 206 | 207 | /// Common logic for `Session.findElement` and `Element.findElement`. 208 | internal func findElement(startingAt subtreeRoot: Element?, locator: ElementLocator, waitTimeout: TimeInterval?) throws -> Element { 209 | precondition(subtreeRoot == nil || subtreeRoot?.session === self) 210 | 211 | return try withImplicitWaitTimeout(waitTimeout) { 212 | let request = Requests.SessionElement(session: id, element: subtreeRoot?.id, locator: locator) 213 | 214 | do { 215 | return try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) { 216 | do { 217 | // Allow errors to bubble up unless they are specifically saying that the element was not found. 218 | let elementId = try webDriver.send(request).value.element 219 | return .success(Element(session: self, id: elementId)) 220 | } catch let error as ErrorResponse where error.status == .noSuchElement { 221 | // Return instead of throwing to indicate that `poll` can retry as needed. 222 | return .failure(error) 223 | } 224 | } 225 | } catch { 226 | throw NoSuchElementError(locator: locator, sourceError: error) 227 | } 228 | } 229 | } 230 | 231 | /// Common logic for `Session.findElements` and `Element.findElements`. 232 | internal func findElements(startingAt element: Element?, locator: ElementLocator, waitTimeout: TimeInterval?) throws -> [Element] { 233 | try withImplicitWaitTimeout(waitTimeout) { 234 | let request = Requests.SessionElements(session: id, element: element?.id, locator: locator) 235 | 236 | do { 237 | return try poll(timeout: emulateImplicitWait ? (waitTimeout ?? _implicitWaitTimeout) : TimeInterval.zero) { 238 | do { 239 | // Allow errors to bubble up unless they are specifically saying that the element was not found. 240 | return .success(try webDriver.send(request).value.map { Element(session: self, id: $0.element) }) 241 | } catch let error as ErrorResponse where error.status == .noSuchElement { 242 | // Follow the WebDriver spec and keep polling if no elements are found. 243 | // Return instead of throwing to indicate that `poll` can retry as needed. 244 | return .failure(error) 245 | } 246 | } 247 | } catch let error as ErrorResponse where error.status == .noSuchElement { 248 | return [] 249 | } 250 | } 251 | } 252 | 253 | /// - Parameters: 254 | /// - waitTimeout: Optional value to override defaultRetryTimeout. 255 | /// - xSpeed: The x speed in pixels per second. 256 | /// - ySpeed: The y speed in pixels per second. 257 | public func flick(xSpeed: Double, ySpeed: Double) throws { 258 | try webDriver.send(Requests.SessionTouchFlick(session: id, xSpeed: xSpeed, ySpeed: ySpeed)) 259 | } 260 | 261 | /// Moves the pointer to a location relative to the current pointer position or an element. 262 | /// - Parameter element: if not nil the top left of the element provides the origin. 263 | /// - Parameter xOffset: x offset from the left of the element. 264 | /// - Parameter yOffset: y offset from the top of the element. 265 | public func moveTo(element: Element? = nil, xOffset: Int = 0, yOffset: Int = 0) throws { 266 | precondition(element?.session == nil || element?.session === self) 267 | try webDriver.send(Requests.SessionMoveTo( 268 | session: id, element: element?.id, xOffset: xOffset, yOffset: yOffset)) 269 | } 270 | 271 | /// Presses down one of the mouse buttons. 272 | /// - Parameter button: The button to be pressed. 273 | public func buttonDown(button: MouseButton = .left) throws { 274 | try webDriver.send(Requests.SessionButton( 275 | session: id, action: .buttonDown, button: button)) 276 | } 277 | 278 | /// Releases one of the mouse buttons. 279 | /// - Parameter button: The button to be released. 280 | public func buttonUp(button: MouseButton = .left) throws { 281 | try webDriver.send(Requests.SessionButton( 282 | session: id, action: .buttonUp, button: button)) 283 | } 284 | 285 | /// Clicks one of the mouse buttons. 286 | /// - Parameter button: The button to be clicked. 287 | public func click(button: MouseButton = .left) throws { 288 | try webDriver.send(Requests.SessionButton( 289 | session: id, action: .click, button: button)) 290 | } 291 | 292 | /// Double clicks the mouse at the current location. 293 | public func doubleClick() throws { 294 | try webDriver.send(Requests.SessionDoubleClick(session: id)) 295 | } 296 | 297 | /// Starts a touch point at a coordinate in this session. 298 | public func touchDown(x: Int, y: Int) throws { 299 | try webDriver.send(Requests.SessionTouchAt(session: id, action: .down, x: x, y: y)) 300 | } 301 | 302 | /// Releases a touch point at a coordinate in this session. 303 | public func touchUp(x: Int, y: Int) throws { 304 | try webDriver.send(Requests.SessionTouchAt(session: id, action: .up, x: x, y: y)) 305 | } 306 | 307 | /// Moves a touch point at a coordinate in this session. 308 | public func touchMove(x: Int, y: Int) throws { 309 | try webDriver.send(Requests.SessionTouchAt(session: id, action: .move, x: x, y: y)) 310 | } 311 | 312 | /// Scrolls via touch. 313 | /// - Parameter element: The element providing the screen location where the scroll starts. 314 | /// - Parameter xOffset: The x offset to scroll by, in pixels. 315 | /// - Parameter yOffset: The y offset to scroll by, in pixels. 316 | public func touchScroll(element: Element? = nil, xOffset: Int, yOffset: Int) throws { 317 | precondition(element?.session == nil || element?.session === self) 318 | try webDriver.send(Requests.SessionTouchScroll( 319 | session: id, element: element?.id, xOffset: xOffset, yOffset: yOffset)) 320 | } 321 | 322 | /// Sends key presses to this session. 323 | /// - Parameter keys: A key sequence according to the WebDriver spec. 324 | /// - Parameter releaseModifiers: A boolean indicating whether to release modifier keys at the end of the sequence. 325 | public func sendKeys(_ keys: Keys, releaseModifiers: Bool = true) throws { 326 | let value = releaseModifiers ? [keys.rawValue, Keys.releaseModifiers.rawValue] : [keys.rawValue] 327 | try webDriver.send(Requests.SessionKeys(session: id, value: value)) 328 | } 329 | 330 | /// Change focus to another window. 331 | /// - Parameter name: The window to change focus to. 332 | public func focus(window name: String) throws { 333 | try webDriver.send(Requests.SessionWindow.Post(session: id, name: name)) 334 | } 335 | 336 | /// Close selected window. 337 | /// - Parameter name: The selected window to close. 338 | public func close(window name: String) throws { 339 | try webDriver.send(Requests.SessionWindow.Delete(session: id, name: name)) 340 | } 341 | 342 | public func window(handle: String) throws -> Window { .init(session: self, handle: handle) } 343 | 344 | /// - Parameter: Orientation the window will flip to {LANDSCAPE|PORTRAIT}. 345 | public func setOrientation(_ value: ScreenOrientation) throws { 346 | try webDriver.send(Requests.SessionOrientation.Post(session: id, orientation: value)) 347 | } 348 | 349 | /// Get the current page source. 350 | public var source: String { 351 | get throws { 352 | try webDriver.send(Requests.SessionSource(session: id)).value 353 | } 354 | } 355 | 356 | /// - Returns: Current window handle. 357 | public var windowHandle: String { 358 | get throws { 359 | let response = try webDriver.send(Requests.SessionWindowHandle(session: id)) 360 | return response.value 361 | } 362 | } 363 | 364 | /// Set the current geolocation. 365 | public func setLocation(_ location: Location) throws { 366 | try webDriver.send(Requests.SessionLocation.Post(session: id, location: location)) 367 | } 368 | 369 | public func setLocation(latitude: Double, longitude: Double, altitude: Double) throws { 370 | try setLocation(Location(latitude: latitude, longitude: longitude, altitude: altitude)) 371 | } 372 | 373 | /// - Returns: Array of window handles. 374 | public var windowHandles: [String] { 375 | get throws { 376 | let response = try webDriver.send(Requests.SessionWindowHandles(session: id)) 377 | return response.value 378 | } 379 | } 380 | 381 | /// Deletes the current session. 382 | public func delete() throws { 383 | guard shouldDelete else { return } 384 | try webDriver.send(Requests.SessionDelete(session: id)) 385 | shouldDelete = false 386 | } 387 | 388 | /// Sends an interaction request, retrying until it is conclusive or the timeout elapses. 389 | internal func sendInteraction(_ request: Req, retryTimeout: TimeInterval? = nil) throws where Req.Response == CodableNone { 390 | try poll(timeout: retryTimeout ?? implicitInteractionRetryTimeout) { 391 | do { 392 | // Immediately bubble most failures, only retry if inconclusive. 393 | try webDriver.send(request) 394 | return .success(()) 395 | } catch let error as ErrorResponse where webDriver.isInconclusiveInteraction(error: error.status) { 396 | // Return instead of throwing to indicate that `poll` can retry as needed. 397 | return .failure(error) 398 | } 399 | } 400 | } 401 | 402 | deinit { 403 | try? delete() // Call `delete` directly to handle errors. 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /Sources/WebDriver/TimeoutType.swift: -------------------------------------------------------------------------------- 1 | public enum TimeoutType: String, Codable { 2 | case script 3 | case implicitWait = "implicit" 4 | case pageLoad = "page load" 5 | } 6 | -------------------------------------------------------------------------------- /Sources/WebDriver/TouchClickKind.swift: -------------------------------------------------------------------------------- 1 | public enum TouchClickKind: String, Codable { 2 | case single 3 | case double 4 | case long 5 | 6 | private enum CodingKeys: String, CodingKey { 7 | case single = "click" 8 | case double = "doubleclick" 9 | case long = "longclick" 10 | } 11 | } -------------------------------------------------------------------------------- /Sources/WebDriver/URLRequestExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | extension URLSession { 7 | func dataTask( 8 | with request: URLRequest, 9 | _ completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void 10 | ) 11 | -> URLSessionDataTask { 12 | dataTask(with: request) { data, response, error in 13 | if let error { 14 | completion(.failure(error)) 15 | } else if let data, let response = response as? HTTPURLResponse { 16 | completion(.success((data, response))) 17 | } else { 18 | fatalError("Unexpected result from URLSessionDataTask.") 19 | } 20 | } 21 | } 22 | } 23 | 24 | extension URLRequest { 25 | func send() throws -> (Int, Data) { 26 | var result: Result<(Data, HTTPURLResponse), Error> = 27 | .failure(NSError(domain: NSURLErrorDomain, code: URLError.unknown.rawValue)) 28 | let semaphore: DispatchSemaphore = .init(value: 0) 29 | let task = URLSession.shared.dataTask(with: self) { 30 | result = $0 31 | semaphore.signal() 32 | } 33 | task.resume() 34 | semaphore.wait() 35 | 36 | switch result { 37 | case let .failure(error): 38 | throw error 39 | case let .success((data, response)): 40 | return (statusCode: response.statusCode, data) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/WebDriver/WebDriver.swift: -------------------------------------------------------------------------------- 1 | public protocol WebDriver { 2 | /// The protocol supported by the WebDriver server. 3 | var wireProtocol: WireProtocol { get } 4 | 5 | /// Sends a WebDriver request to the server and returns the response. 6 | /// - Parameter request: The request to send. 7 | @discardableResult 8 | func send(_ request: Req) throws -> Req.Response 9 | 10 | /// Determines if a given error is inconclusive and should be retried. 11 | func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool 12 | } 13 | 14 | extension WebDriver { 15 | /// status - returns WinAppDriver status 16 | /// Returns: an instance of the WebDriverStatus type 17 | public var status: WebDriverStatus { 18 | get throws { 19 | switch wireProtocol { 20 | case .legacySelenium: return try send(Requests.LegacySelenium.Status()) 21 | case .w3c: return try send(Requests.W3C.Status()).value 22 | } 23 | } 24 | } 25 | 26 | public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool { false } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/WebDriver/WebDriverStatus.swift: -------------------------------------------------------------------------------- 1 | public struct WebDriverStatus: Codable { 2 | // From WebDriver spec 3 | public var ready: Bool? 4 | public var message: String? 5 | 6 | // From Selenium's legacy json protocol 7 | public var build: Build? 8 | public var os: OS? 9 | 10 | public struct Build: Codable { 11 | public var revision: String? 12 | public var time: String? 13 | public var version: String? 14 | } 15 | 16 | public struct OS: Codable { 17 | public var arch: String? 18 | public var name: String? 19 | public var version: String? 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/WebDriver/Window.swift: -------------------------------------------------------------------------------- 1 | /// Exposes window-specific webdriver operations 2 | public struct Window { 3 | var webDriver: WebDriver { session.webDriver } 4 | public let session: Session 5 | public let handle: String 6 | 7 | public init(session: Session, handle: String) { 8 | self.session = session 9 | self.handle = handle 10 | } 11 | 12 | public var position: (x: Double, y: Double) { 13 | get throws { 14 | let responseValue = try webDriver.send(Requests.WindowPosition.Get( 15 | session: session.id, windowHandle: handle)).value 16 | return (responseValue.x, responseValue.y) 17 | } 18 | } 19 | 20 | public var size: (width: Double, height: Double) { 21 | get throws { 22 | let responseValue = try webDriver.send(Requests.WindowSize.Get( 23 | session: session.id, windowHandle: handle)).value 24 | return (responseValue.width, responseValue.height) 25 | } 26 | } 27 | 28 | /// - Parameters: 29 | /// - width: The new window width 30 | /// - height: The new window height 31 | public func setSize(width: Double, height: Double) throws { 32 | try webDriver.send(Requests.WindowSize.Post(session: session.id, windowHandle: handle, width: width, height: height)) 33 | } 34 | 35 | /// - Parameters: 36 | /// - x: Position in the top left corner of the x coordinate 37 | /// - y: Position in the top left corner of the y coordinate 38 | public func setPosition(x: Double, y: Double) throws { 39 | try webDriver.send(Requests.WindowPosition.Post(session: session.id, windowHandle: handle, x: x, y: y)) 40 | } 41 | 42 | /// Maximize specific window if :windowHandle is "current" the current window will be maximized 43 | public func maximize() throws { 44 | try webDriver.send(Requests.WindowMaximize(session: session.id, windowHandle: handle)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/WebDriver/WireProtocol.swift: -------------------------------------------------------------------------------- 1 | public enum WireProtocol { 2 | /// Identifiers Selenium's Legacy JSON Wire Protocol, 3 | /// Documented at: https://www.selenium.dev/documentation/legacy/json_wire_protocol 4 | case legacySelenium 5 | /// Identifiers the W3C WebDriver Protocol, 6 | /// Documented at: https://w3c.github.io/webdriver/webdriver-spec.html 7 | case w3c 8 | } -------------------------------------------------------------------------------- /Sources/WinAppDriver/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(WinAppDriver 2 | CommandLine.swift 3 | ElementLocator+accessibilityId.swift 4 | ErrorResponse+WinAppDriver.swift 5 | ReexportWebDriver.swift 6 | Win32Error.swift 7 | Win32ProcessTree.swift 8 | WinAppDriver+Attributes.swift 9 | WinAppDriver+Capabilities.swift 10 | WinAppDriver.swift 11 | WindowsSystemPaths.swift) 12 | target_link_libraries(WinAppDriver PRIVATE 13 | WebDriver) 14 | -------------------------------------------------------------------------------- /Sources/WinAppDriver/CommandLine.swift: -------------------------------------------------------------------------------- 1 | 2 | // Build the argument string of a command line from an array of arguments, 3 | // properly escaping each argument. 4 | func buildCommandLineArgsString(args: [String]) -> String { 5 | func escapeArg(arg: String) -> String { 6 | var escapedArg = arg.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") // Escape backslashes and double quotes 7 | if escapedArg.contains(" ") || arg.contains("\t") { 8 | escapedArg = "\"\(escapedArg)\"" // quote args with spaces 9 | } 10 | return escapedArg 11 | } 12 | 13 | return args.map(escapeArg).joined(separator: " ") 14 | } 15 | -------------------------------------------------------------------------------- /Sources/WinAppDriver/ElementLocator+accessibilityId.swift: -------------------------------------------------------------------------------- 1 | import WebDriver 2 | 3 | extension ElementLocator { 4 | /// Matches an element whose accessibility ID matches the search value. 5 | public static func accessibilityId(_ value: String) -> Self { 6 | Self(using: "accessibility id", value: value) 7 | } 8 | } -------------------------------------------------------------------------------- /Sources/WinAppDriver/ErrorResponse+WinAppDriver.swift: -------------------------------------------------------------------------------- 1 | import WebDriver 2 | 3 | extension ErrorResponse.Status { 4 | // WinAppDriver returns when passing an incorrect window handle to attach to. 5 | static let winAppDriver_invalidArgument = Self(rawValue: 100) 6 | 7 | /// Indicates that a request could not be completed because the element is not pointer- or keyboard interactable. 8 | static let winAppDriver_elementNotInteractable = Self(rawValue: 105) 9 | } 10 | -------------------------------------------------------------------------------- /Sources/WinAppDriver/ReexportWebDriver.swift: -------------------------------------------------------------------------------- 1 | @_exported import WebDriver -------------------------------------------------------------------------------- /Sources/WinAppDriver/Win32Error.swift: -------------------------------------------------------------------------------- 1 | import WinSDK 2 | 3 | internal struct Win32Error: Error { 4 | public var apiName: String 5 | public var errorCode: UInt32 6 | 7 | internal static func getLastError(apiName: String) -> Self { 8 | Self(apiName: apiName, errorCode: GetLastError()) 9 | } 10 | } -------------------------------------------------------------------------------- /Sources/WinAppDriver/Win32ProcessTree.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.TimeInterval 2 | import WinSDK 3 | 4 | /// Options for launching a process. 5 | /// Note that not setting all of the stdoutHandle, stderrHandle, and stdinHandle 6 | /// fields will result in the process inheriting the parent's stdout, stderr or 7 | /// stdin handles, respectively. This may result in the process's output being 8 | /// written to the parent's console, even if `spawnNewConsole` is set to `true`. 9 | internal struct ProcessLaunchOptions { 10 | /// Spawn a new console for the process. 11 | public var spawnNewConsole: Bool = true 12 | /// Redirect the process's stdout to the given handle. 13 | public var stdoutHandle: HANDLE? = nil 14 | /// Redirect the process's stderr to the given handle. 15 | public var stderrHandle: HANDLE? = nil 16 | /// Redirect the process's stdin to the given handle. 17 | public var stdinHandle: HANDLE? = nil 18 | } 19 | 20 | /// Starts and tracks the lifetime of a process tree using Win32 APIs. 21 | internal class Win32ProcessTree { 22 | internal let jobHandle: HANDLE 23 | internal let handle: HANDLE 24 | 25 | init(path: String, args: [String], options: ProcessLaunchOptions = ProcessLaunchOptions()) 26 | throws { 27 | // Use a job object to ensure that the process tree doesn't outlive us. 28 | jobHandle = try Self.createJobObject() 29 | 30 | let commandLine = buildCommandLineArgsString(args: [path] + args) 31 | do { 32 | handle = try Self.createProcessInJob( 33 | commandLine: commandLine, jobHandle: jobHandle, options: options) 34 | } catch { 35 | CloseHandle(jobHandle) 36 | throw error 37 | } 38 | } 39 | 40 | var exitCode: DWORD? { 41 | get throws { 42 | var result: DWORD = 0 43 | guard WinSDK.GetExitCodeProcess(handle, &result) else { 44 | throw Win32Error.getLastError(apiName: "GetExitCodeProcess") 45 | } 46 | return result == WinSDK.STILL_ACTIVE ? nil : result 47 | } 48 | } 49 | 50 | func terminate(waitTime: TimeInterval?) throws { 51 | precondition((waitTime ?? 0) >= 0) 52 | 53 | if !TerminateJobObject(jobHandle, UINT.max) { 54 | throw Win32Error.getLastError(apiName: "TerminateJobObject") 55 | } 56 | 57 | if let waitTime { 58 | let milliseconds = waitTime * 1000 59 | let millisecondsDword = milliseconds > Double(DWORD.max) ? INFINITE : DWORD(milliseconds) 60 | let waitResult = WaitForSingleObject(handle, millisecondsDword) 61 | assert(waitResult == WAIT_OBJECT_0, "The process did not terminate within the expected time interval.") 62 | } 63 | } 64 | 65 | deinit { 66 | CloseHandle(handle) 67 | CloseHandle(jobHandle) 68 | } 69 | 70 | private static func createJobObject() throws -> HANDLE { 71 | guard let jobHandle = CreateJobObjectW(nil, nil) else { 72 | throw Win32Error.getLastError(apiName: "CreateJobObjectW") 73 | } 74 | 75 | var limitInfo = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() 76 | limitInfo.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) | DWORD(JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK) 77 | guard SetInformationJobObject(jobHandle, JobObjectExtendedLimitInformation, 78 | &limitInfo, DWORD(MemoryLayout.size)) else { 79 | defer { CloseHandle(jobHandle) } 80 | throw Win32Error.getLastError(apiName: "SetInformationJobObject") 81 | } 82 | 83 | return jobHandle 84 | } 85 | 86 | private static func createProcessInJob( 87 | commandLine: String, 88 | jobHandle: HANDLE, 89 | options: ProcessLaunchOptions 90 | ) throws -> HANDLE { 91 | try commandLine.withCString(encodedAs: UTF16.self) { commandLine throws in 92 | var startupInfo = STARTUPINFOW() 93 | startupInfo.cb = DWORD(MemoryLayout.size) 94 | var redirectStdHandle = false 95 | 96 | let creationFlags = 97 | DWORD(CREATE_SUSPENDED) | DWORD(CREATE_NEW_PROCESS_GROUP) 98 | | (options.spawnNewConsole ? DWORD(CREATE_NEW_CONSOLE) : 0) 99 | 100 | // Populate the startup info struct with the handles to redirect. 101 | // Note that these fields are unused if `STARTF_USESTDHANDLES` is 102 | // not set. 103 | if let stdoutHandle = options.stdoutHandle { 104 | startupInfo.hStdOutput = stdoutHandle 105 | redirectStdHandle = true 106 | } else { 107 | startupInfo.hStdOutput = GetStdHandle(DWORD(STD_OUTPUT_HANDLE)) 108 | } 109 | if let stderrHandle = options.stderrHandle { 110 | startupInfo.hStdError = stderrHandle 111 | redirectStdHandle = true 112 | } else { 113 | startupInfo.hStdError = GetStdHandle(DWORD(STD_ERROR_HANDLE)) 114 | } 115 | if let stdinHandle = options.stdinHandle { 116 | startupInfo.hStdInput = stdinHandle 117 | redirectStdHandle = true 118 | } else { 119 | startupInfo.hStdInput = GetStdHandle(DWORD(STD_INPUT_HANDLE)) 120 | } 121 | if redirectStdHandle { 122 | startupInfo.dwFlags |= DWORD(STARTF_USESTDHANDLES) 123 | } 124 | 125 | var processInfo = PROCESS_INFORMATION() 126 | guard CreateProcessW( 127 | nil, 128 | UnsafeMutablePointer(mutating: commandLine), 129 | nil, 130 | nil, 131 | redirectStdHandle, // Inherit handles is necessary for redirects. 132 | creationFlags, 133 | nil, 134 | nil, 135 | &startupInfo, 136 | &processInfo 137 | ) else { 138 | throw Win32Error.getLastError(apiName: "CreateProcessW") 139 | } 140 | 141 | defer { CloseHandle(processInfo.hThread) } 142 | 143 | guard AssignProcessToJobObject(jobHandle, processInfo.hProcess) else { 144 | defer { CloseHandle(processInfo.hProcess) } 145 | throw Win32Error.getLastError(apiName: "AssignProcessToJobObject") 146 | } 147 | 148 | guard ResumeThread(processInfo.hThread) != DWORD.max else { 149 | defer { CloseHandle(processInfo.hProcess) } 150 | throw Win32Error.getLastError(apiName: "ResumeThread") 151 | } 152 | 153 | return processInfo.hProcess 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/WinAppDriver/WinAppDriver+Attributes.swift: -------------------------------------------------------------------------------- 1 | extension WinAppDriver { 2 | public enum Attributes { 3 | // Automation Identifiers from UIAutomationCoreApi.h 4 | public static let boundingRectangle = "BoundingRectangle" 5 | public static let processId = "ProcessId" 6 | public static let controlType = "ControlType" 7 | public static let localizedControlType = "LocalizedControlType" 8 | public static let name = "Name" 9 | public static let acceleratorKey = "AcceleratorKey" 10 | public static let accessKey = "AccessKey" 11 | public static let hasKeyboardFocus = "HasKeyboardFocus" 12 | public static let isKeyboardFocusable = "IsKeyboardFocusable" 13 | public static let isEnabled = "IsEnabled" 14 | public static let automationId = "AutomationId" 15 | public static let className = "ClassName" 16 | public static let helpText = "HelpText" 17 | public static let clickablePoint = "ClickablePoint" 18 | public static let culture = "Culture" 19 | public static let isControlElement = "IsControlElement" 20 | public static let isContentElement = "IsContentElement" 21 | public static let labeledBy = "LabeledBy" 22 | public static let isPassword = "IsPassword" 23 | public static let newNativeWindowHandle = "NewNativeWindowHandle" 24 | public static let itemType = "ItemType" 25 | public static let isOffscreen = "IsOffscreen" 26 | public static let orientation = "Orientation" 27 | public static let frameworkId = "FrameworkId" 28 | public static let isRequiredForForm = "IsRequiredForForm" 29 | public static let itemStatus = "ItemStatus" 30 | public static let ariaRole = "AriaRole" 31 | public static let ariaProperties = "AriaProperties" 32 | public static let isDataValidForForm = "IsDataValidForForm" 33 | public static let controllerFor = "ControllerFor" 34 | public static let describedBy = "DescribedBy" 35 | public static let flowsTo = "FlowsTo" 36 | public static let providerDescription = "ProviderDescription" 37 | public static let optimizeForVisualContent = "OptimizeForVisualContent" 38 | public static let isDockPatternAvailable = "IsDockPatternAvailable" 39 | public static let isExpandCollapsePatternAvailable = "IsExpandCollapsePatternAvailable" 40 | public static let isGridItemPatternAvailable = "IsGridItemPatternAvailable" 41 | public static let isGridPatternAvailable = "IsGridPatternAvailable" 42 | public static let isInvokePatternAvailable = "IsInvokePatternAvailable" 43 | public static let isMultipleViewPatternAvailable = "IsMultipleViewPatternAvailable" 44 | public static let isRangeValuePatternAvailable = "IsRangeValuePatternAvailable" 45 | public static let isScrollPatternAvailable = "IsScrollPatternAvailable" 46 | public static let isScrollItemPatternAvailable = "IsScrollItemPatternAvailable" 47 | public static let isSelectionItemPatternAvailable = "IsSelectionItemPatternAvailable" 48 | public static let isSelectionPatternAvailable = "IsSelectionPatternAvailable" 49 | public static let isTablePatternAvailable = "IsTablePatternAvailable" 50 | public static let isTableItemPatternAvailable = "IsTableItemPatternAvailable" 51 | public static let isTextPatternAvailable = "IsTextPatternAvailable" 52 | public static let isTogglePatternAvailable = "IsTogglePatternAvailable" 53 | public static let isTransformPatternAvailable = "IsTransformPatternAvailable" 54 | public static let isValuePatternAvailable = "IsValuePatternAvailable" 55 | public static let isWindowPatternAvailable = "IsWindowPatternAvailable" 56 | public static let isLegacyIAccessiblePatternAvailable = "IsLegacyIAccessiblePatternAvailable" 57 | public static let isItemContainerPatternAvailable = "IsItemContainerPatternAvailable" 58 | public static let isVirtualizedItemPatternAvailable = "IsVirtualizedItemPatternAvailable" 59 | public static let isSynchronizedInputPatternAvailable = "IsSynchronizedInputPatternAvailable" 60 | public static let isObjectModelPatternAvailable = "IsObjectModelPatternAvailable" 61 | public static let isAnnotationPatternAvailable = "IsAnnotationPatternAvailable" 62 | public static let isTextPattern2Available = "IsTextPattern2Available" 63 | public static let isTextEditPatternAvailable = "IsTextEditPatternAvailable" 64 | public static let isCustomNavigationPatternAvailable = "IsCustomNavigationPatternAvailable" 65 | public static let isStylesPatternAvailable = "IsStylesPatternAvailable" 66 | public static let isSpreadsheetPatternAvailable = "IsSpreadsheetPatternAvailable" 67 | public static let isSpreadsheetItemPatternAvailable = "IsSpreadsheetItemPatternAvailable" 68 | public static let isTransformPattern2Available = "IsTransformPattern2Available" 69 | public static let isTextChildPatternAvailable = "IsTextChildPatternAvailable" 70 | public static let isDragPatternAvailable = "IsDragPatternAvailable" 71 | public static let isDropTargetPatternAvailable = "IsDropTargetPatternAvailable" 72 | public static let isStructuredMarkupPatternAvailable = "IsStructuredMarkupPatternAvailable" 73 | public static let isPeripheral = "IsPeripheral" 74 | public static let positionInSet = "PositionInSet" 75 | public static let sizeOfSet = "SizeOfSet" 76 | public static let level = "Level" 77 | public static let annotationTypes = "AnnotationTypes" 78 | public static let annotationObjects = "AnnotationObjects" 79 | public static let landmarkType = "LandmarkType" 80 | public static let localizedLandmarkType = "LocalizedLandmarkType" 81 | public static let fullDescription = "FullDescription" 82 | public static let headingLevel = "HeadingLevel" 83 | public static let isDialog = "IsDialog" 84 | } 85 | } -------------------------------------------------------------------------------- /Sources/WinAppDriver/WinAppDriver+Capabilities.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.TimeInterval 2 | import WebDriver 3 | 4 | extension WinAppDriver { 5 | // See https://github.com/microsoft/WinAppDriver/blob/master/Docs/AuthoringTestScripts.md 6 | public class Capabilities: BaseCapabilities { 7 | public var app: String? 8 | public var appArguments: String? 9 | public var appTopLevelWindow: String? 10 | public var appWorkingDir: String? 11 | public var platformVersion: String? 12 | public var waitForAppLaunch: Int? 13 | public var experimentalWebDriver: Bool? 14 | 15 | public override init() { super.init() } 16 | 17 | public static func startApp(name: String, arguments: [String] = [], workingDir: String? = nil, waitTime: TimeInterval? = nil) -> Capabilities { 18 | let caps = Capabilities() 19 | caps.app = name 20 | caps.appArguments = arguments.isEmpty ? nil : buildCommandLineArgsString(args: arguments) 21 | caps.appWorkingDir = workingDir 22 | if let waitTime { caps.waitForAppLaunch = Int(waitTime * 1000) } 23 | return caps 24 | } 25 | 26 | public static func attachToApp(topLevelWindowHandle: UInt) -> Capabilities { 27 | let caps = Capabilities() 28 | caps.appTopLevelWindow = String(topLevelWindowHandle, radix: 16) 29 | return caps 30 | } 31 | 32 | public static func attachToDesktop() -> Capabilities { 33 | let caps = Capabilities() 34 | caps.app = "Root" 35 | return caps 36 | } 37 | 38 | // Swift can't synthesize init(from:) for subclasses of Codable classes 39 | public required init(from decoder: Decoder) throws { 40 | try super.init(from: decoder) 41 | let container = try decoder.container(keyedBy: CodingKeys.self) 42 | app = try? container.decodeIfPresent(String.self, forKey: .app) 43 | appArguments = try? container.decodeIfPresent(String.self, forKey: .appArguments) 44 | appTopLevelWindow = try? container.decodeIfPresent(String.self, forKey: .appTopLevelWindow) 45 | appWorkingDir = try? container.decodeIfPresent(String.self, forKey: .appWorkingDir) 46 | platformVersion = try? container.decodeIfPresent(String.self, forKey: .platformVersion) 47 | waitForAppLaunch = try? container.decodeIfPresent(Int.self, forKey: .waitForAppLaunch) 48 | experimentalWebDriver = try? container.decodeIfPresent(Bool.self, forKey: .experimentalWebDriver) 49 | } 50 | 51 | public override func encode(to encoder: Encoder) throws { 52 | try super.encode(to: encoder) 53 | var container = encoder.container(keyedBy: CodingKeys.self) 54 | try container.encodeIfPresent(app, forKey: .app) 55 | try container.encodeIfPresent(appArguments, forKey: .appArguments) 56 | try container.encodeIfPresent(appTopLevelWindow, forKey: .appTopLevelWindow) 57 | try container.encodeIfPresent(appWorkingDir, forKey: .appWorkingDir) 58 | try container.encodeIfPresent(platformVersion, forKey: .platformVersion) 59 | try container.encodeIfPresent(waitForAppLaunch, forKey: .waitForAppLaunch) 60 | try container.encodeIfPresent(experimentalWebDriver, forKey: .experimentalWebDriver) 61 | } 62 | 63 | private enum CodingKeys: String, CodingKey { 64 | case app 65 | case appArguments 66 | case appTopLevelWindow 67 | case appWorkingDir 68 | case platformVersion 69 | case waitForAppLaunch = "ms:waitForAppLaunch" 70 | case experimentalWebDriver = "ms:experimental-webdriver" 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Sources/WinAppDriver/WinAppDriver.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import WebDriver 4 | import WinSDK 5 | 6 | public class WinAppDriver: WebDriver { 7 | /// Raised when the WinAppDriver.exe process fails to start 8 | public struct StartError: Error { 9 | public var message: String 10 | } 11 | 12 | public static let defaultIp = "127.0.0.1" 13 | public static let defaultPort = 4723 14 | public static let executableName = "WinAppDriver.exe" 15 | public static var defaultExecutablePath: String { 16 | "\(WindowsSystemPaths.programFilesX86)\\Windows Application Driver\\\(executableName)" 17 | } 18 | public static let defaultStartWaitTime: TimeInterval = 1.0 19 | 20 | private let httpWebDriver: HTTPWebDriver 21 | private var processTree: Win32ProcessTree? 22 | /// The write end of a pipe that is connected to the child process's stdin. 23 | private var childStdinHandle: HANDLE? 24 | 25 | private init( 26 | httpWebDriver: HTTPWebDriver, 27 | processTree: Win32ProcessTree? = nil, 28 | childStdinHandle: HANDLE? = nil) { 29 | self.httpWebDriver = httpWebDriver 30 | self.processTree = processTree 31 | self.childStdinHandle = childStdinHandle 32 | } 33 | 34 | public static func attach(ip: String = defaultIp, port: Int = defaultPort) -> WinAppDriver { 35 | let httpWebDriver = HTTPWebDriver(endpoint: URL(string: "http://\(ip):\(port)")!, wireProtocol: .legacySelenium) 36 | return WinAppDriver(httpWebDriver: httpWebDriver) 37 | } 38 | 39 | public static func start( 40 | executablePath: String = defaultExecutablePath, 41 | ip: String = defaultIp, 42 | port: Int = defaultPort, 43 | waitTime: TimeInterval? = defaultStartWaitTime, 44 | outputFile: String? = nil) throws -> WinAppDriver { 45 | let processTree: Win32ProcessTree 46 | var childStdinHandle: HANDLE? = nil 47 | do { 48 | var launchOptions = ProcessLaunchOptions() 49 | 50 | // Close our handles when the process has launched. The child process keeps a copy. 51 | defer { 52 | if let handle = launchOptions.stdoutHandle { 53 | CloseHandle(handle) 54 | } 55 | if let handle = launchOptions.stdinHandle { 56 | CloseHandle(handle) 57 | } 58 | } 59 | 60 | if let outputFile = outputFile { 61 | // Open the output file for writing to the child stdout. 62 | var securityAttributes = SECURITY_ATTRIBUTES() 63 | securityAttributes.nLength = DWORD(MemoryLayout.size) 64 | securityAttributes.bInheritHandle = true 65 | launchOptions.stdoutHandle = outputFile.withCString(encodedAs: UTF16.self) { 66 | outputFile in 67 | CreateFileW( 68 | UnsafeMutablePointer(mutating: outputFile), DWORD(GENERIC_WRITE), 69 | DWORD(FILE_SHARE_READ), &securityAttributes, 70 | DWORD(OPEN_ALWAYS), DWORD(FILE_ATTRIBUTE_NORMAL), nil) 71 | } 72 | if launchOptions.stdoutHandle == INVALID_HANDLE_VALUE { 73 | // Failed to open the output file for writing. 74 | throw Win32Error.getLastError(apiName: "CreateFileW") 75 | } 76 | 77 | // Use the same handle for stderr. 78 | launchOptions.stderrHandle = launchOptions.stdoutHandle 79 | 80 | // WinAppDriver will close immediately if no stdin is provided so create a dummy 81 | // pipe here to keep stdin open until the child process is closed. 82 | var childReadInputHandle: HANDLE? 83 | if !CreatePipe(&childReadInputHandle, &childStdinHandle, &securityAttributes, 0) { 84 | throw Win32Error.getLastError(apiName: "CreatePipe") 85 | } 86 | launchOptions.stdinHandle = childReadInputHandle 87 | 88 | // Also use the parent console to stop spurious new consoles from spawning. 89 | launchOptions.spawnNewConsole = false 90 | } 91 | 92 | processTree = try Win32ProcessTree( 93 | path: executablePath, args: [ip, String(port)], options: launchOptions) 94 | } catch let error as Win32Error { 95 | CloseHandle(childStdinHandle) 96 | throw StartError(message: "Call to Win32 \(error.apiName) failed with error code \(error.errorCode).") 97 | } 98 | 99 | let httpWebDriver = HTTPWebDriver(endpoint: URL(string: "http://\(ip):\(port)")!, wireProtocol: .legacySelenium) 100 | 101 | // Give WinAppDriver some time to start up 102 | if let waitTime { 103 | // TODO(#40): This should be using polling, but an immediate url request would block forever 104 | Thread.sleep(forTimeInterval: waitTime) 105 | 106 | if let earlyExitCode = try? processTree.exitCode { 107 | throw StartError(message: "WinAppDriver process exited early with error code \(earlyExitCode).") 108 | } 109 | } 110 | 111 | return WinAppDriver( 112 | httpWebDriver: httpWebDriver, 113 | processTree: processTree, 114 | childStdinHandle: childStdinHandle) 115 | } 116 | 117 | deinit { 118 | try? close() // Call close() directly to handle errors. 119 | } 120 | 121 | public var wireProtocol: WireProtocol { .legacySelenium } 122 | 123 | @discardableResult 124 | public func send(_ request: Req) throws -> Req.Response { 125 | try httpWebDriver.send(request) 126 | } 127 | 128 | public func isInconclusiveInteraction(error: ErrorResponse.Status) -> Bool { 129 | error == .winAppDriver_elementNotInteractable || httpWebDriver.isInconclusiveInteraction(error: error) 130 | } 131 | 132 | public func close() throws { 133 | if let childStdinHandle { 134 | CloseHandle(childStdinHandle) 135 | self.childStdinHandle = nil 136 | } 137 | 138 | if let processTree { 139 | try processTree.terminate(waitTime: TimeInterval.infinity) 140 | self.processTree = nil 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/WinAppDriver/WindowsSystemPaths.swift: -------------------------------------------------------------------------------- 1 | import WinSDK 2 | 3 | enum WindowsSystemPaths { 4 | // Prefer a fallback to bubbling errors since consuming code 5 | // use these values as constants to build larger paths 6 | // and will handle failures at the time of accessing the file system. 7 | 8 | static let programFilesX86: String = getKnownFolderPath(CSIDL_PROGRAM_FILESX86, fallback: "C:\\Program Files (x86)") 9 | static let windowsDir: String = getKnownFolderPath(CSIDL_WINDOWS, fallback: "C:\\Windows") 10 | static let system32: String = getKnownFolderPath(CSIDL_SYSTEM, fallback: "\(windowsDir)\\System32") 11 | 12 | private static func getKnownFolderPath(_ folderId: Int32, fallback: String) -> String { 13 | (try? getKnownFolderPath(folderId)) ?? fallback 14 | } 15 | 16 | public static func getKnownFolderPath(_ folderID: Int32) throws -> String { 17 | var path = [WCHAR](repeating: 0, count: Int(MAX_PATH + 1)) 18 | // We can't call SHGetKnownFolderPath due to the changing signature 19 | // of KNOWNFOLDERID between C and C++ interop. 20 | // Safe to revert when https://github.com/apple/swift/issues/69157 is fixed. 21 | let result = WinSDK.SHGetFolderPathW(nil, folderID, nil, 0, &path) 22 | guard result == S_OK else { 23 | throw Win32Error.getLastError(apiName: "SHGetFolderPath") 24 | } 25 | return String(decodingCString: path, as: UTF16.self) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/AppiumTests/AppiumTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebDriver 3 | import XCTest 4 | 5 | final class AppiumTests: XCTestCase { 6 | private var appiumServerURL: URL? = nil 7 | 8 | override func setUpWithError() throws { 9 | super.setUp() 10 | appiumServerURL = ProcessInfo.processInfo.environment["APPIUM_SERVER_URL"].flatMap { URL(string: $0) } 11 | try XCTSkipIf(appiumServerURL == nil, "APPIUM_SERVER_URL environment variable is not set.") 12 | } 13 | 14 | #if os(Windows) 15 | func testAppium() throws { 16 | let webDriver = HTTPWebDriver(endpoint: appiumServerURL!, wireProtocol: .w3c) 17 | 18 | let appiumOptions = Capabilities.AppiumOptions() 19 | appiumOptions.automationName = "windows" 20 | appiumOptions.app = "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App" 21 | 22 | let capabilities = Capabilities() 23 | capabilities.platformName = "windows" 24 | capabilities.appiumOptions = appiumOptions 25 | 26 | let session = try Session(webDriver: webDriver, capabilities: capabilities) 27 | try session.sendKeys(.alt(.f4)) 28 | } 29 | #endif 30 | } 31 | -------------------------------------------------------------------------------- /Tests/Common/PNGUtilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func isPNG(data: Data) -> Bool { 4 | // From https://www.w3.org/TR/png/#5PNG-file-signature 5 | let PNGSignature: [UInt8] = [137, 80, 78, 71, 13, 10, 26, 10] 6 | 7 | let range: Range = data.range(of: Data(PNGSignature))! 8 | return range == 0..<8 9 | } 10 | -------------------------------------------------------------------------------- /Tests/UnitTests/APIToRequestMappingTests.swift: -------------------------------------------------------------------------------- 1 | import TestsCommon 2 | @testable import WebDriver 3 | import XCTest 4 | 5 | /// Tests how usage of high-level Session/Element APIs map to lower-level requests 6 | class APIToRequestMappingTests: XCTestCase { 7 | private typealias ResponseWithValue = Requests.ResponseWithValue 8 | 9 | func testCreateSession() throws { 10 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 11 | mockWebDriver.expect(path: "session", method: .post, type: Requests.LegacySelenium.Session.self) { 12 | let capabilities = Capabilities() 13 | capabilities.platformName = "myPlatform" 14 | return Requests.LegacySelenium.Session.Response(sessionId: "mySession", value: capabilities) 15 | } 16 | let session = try Session(webDriver: mockWebDriver, capabilities: Capabilities()) 17 | XCTAssertEqual(session.id, "mySession") 18 | XCTAssertEqual(session.capabilities.platformName, "myPlatform") 19 | 20 | // Account for session deinitializer 21 | mockWebDriver.expect(path: "session/mySession", method: .delete) 22 | } 23 | 24 | func testStatus_legacySelenium() throws { 25 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 26 | mockWebDriver.expect(path: "status", method: .get, type: Requests.LegacySelenium.Status.self) { 27 | var status = WebDriverStatus() 28 | status.ready = true 29 | return status 30 | } 31 | 32 | XCTAssertEqual(try mockWebDriver.status.ready, true) 33 | } 34 | 35 | func testStatus_w3c() throws { 36 | let mockWebDriver = MockWebDriver(wireProtocol: .w3c) 37 | mockWebDriver.expect(path: "status", method: .get, type: Requests.W3C.Status.self) { 38 | var status = WebDriverStatus() 39 | status.ready = true 40 | return Requests.W3C.Status.Response(status) 41 | } 42 | 43 | XCTAssertEqual(try mockWebDriver.status.ready, true) 44 | } 45 | 46 | func testSessionTitle() throws { 47 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 48 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 49 | mockWebDriver.expect(path: "session/mySession/title", method: .get) { 50 | ResponseWithValue("mySession.title") 51 | } 52 | XCTAssertEqual(try session.title, "mySession.title") 53 | } 54 | 55 | func testSessionScreenshot() throws { 56 | let base64TestImage: String = 57 | "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAHCAYAAAA1WQxeAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAB2GAAAdhgFdohOBAAAABmJLR0QA/wD/AP+gvaeTAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIzLTA3LTEzVDIwOjAxOjQ1KzAwOjAwCWqxhgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMy0wNy0xM1QyMDowMTo0NSswMDowMHg3CToAAAC2SURBVBhXY/iPDG7c+///5y8oBwJQFRj4/P9f3QNhn78Appi+fP3LkNfxnIFh43oGBiE+BoYjZxkYHj5iYFi2goHhzVsGpoePfjBMrrzLUNT4jIEh2IaBQZCTgaF1EgODkiIDg4gwA9iKpILL/xnkL/xnkLzyv8UUaIVL2P//Xz5DrGAAgoPzVjDosRxmaG4UZxArjAAa/YGBYfdxkBTEhP37bv9/+eIDWAcYHDsHNOEbkPH/PwCcrZANcnx9SAAAAABJRU5ErkJggg==" 58 | 59 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 60 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 61 | mockWebDriver.expect(path: "session/mySession/screenshot", method: .get) { 62 | ResponseWithValue(base64TestImage) 63 | } 64 | let data: Data = try session.screenshot() 65 | XCTAssert(isPNG(data: data)) 66 | } 67 | 68 | func testSessionFindElement() throws { 69 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 70 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 71 | mockWebDriver.expect(path: "session/mySession/element", method: .post, type: Requests.SessionElement.self) { 72 | XCTAssertEqual($0.using, "name") 73 | XCTAssertEqual($0.value, "myElement.name") 74 | return ResponseWithValue(.init(element: "myElement")) 75 | } 76 | try session.findElement(locator: .name("myElement.name")) 77 | 78 | mockWebDriver.expect(path: "session/mySession/element/active", method: .post, type: Requests.SessionActiveElement.self) { 79 | ResponseWithValue(.init(element: "myElement")) 80 | } 81 | _ = try session.activeElement! 82 | } 83 | 84 | func testSessionMoveTo() throws { 85 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 86 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 87 | let element = Element(session: session, id: "myElement") 88 | mockWebDriver.expect(path: "session/mySession/moveto", method: .post, type: Requests.SessionMoveTo.self) { 89 | XCTAssertEqual($0.element, "myElement") 90 | XCTAssertEqual($0.xOffset, 30) 91 | XCTAssertEqual($0.yOffset, 0) 92 | return CodableNone() 93 | } 94 | try session.moveTo(element: element, xOffset: 30, yOffset: 0) 95 | } 96 | 97 | func testSessionClick() throws { 98 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 99 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 100 | mockWebDriver.expect(path: "session/mySession/click", method: .post, type: Requests.SessionButton.self) { 101 | XCTAssertEqual($0.button, .left) 102 | return CodableNone() 103 | } 104 | try session.click(button: .left) 105 | } 106 | 107 | func testSessionButton() throws { 108 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 109 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 110 | mockWebDriver.expect(path: "session/mySession/buttondown", method: .post, type: Requests.SessionButton.self) { 111 | XCTAssertEqual($0.button, .right) 112 | return CodableNone() 113 | } 114 | try session.buttonDown(button: .right) 115 | 116 | mockWebDriver.expect(path: "session/mySession/buttonup", method: .post, type: Requests.SessionButton.self) { 117 | XCTAssertEqual($0.button, .right) 118 | return CodableNone() 119 | } 120 | try session.buttonUp(button: .right) 121 | } 122 | 123 | func testSessionOrientation() throws { 124 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 125 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 126 | mockWebDriver.expect(path: "session/mySession/orientation", method: .post) 127 | try session.setOrientation(.portrait) 128 | 129 | mockWebDriver.expect(path: "session/mySession/orientation", method: .get, type: Requests.SessionOrientation.Get.self) { 130 | ResponseWithValue(.portrait) 131 | } 132 | XCTAssert(try session.orientation == .portrait) 133 | 134 | mockWebDriver.expect(path: "session/mySession/orientation", method: .post) 135 | try session.setOrientation(.landscape) 136 | 137 | mockWebDriver.expect(path: "session/mySession/orientation", method: .get, type: Requests.SessionOrientation.Get.self) { 138 | ResponseWithValue(.landscape) 139 | } 140 | XCTAssert(try session.orientation == .landscape) 141 | } 142 | 143 | func testSendKeys() throws { 144 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 145 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 146 | let element = Element(session: session, id: "myElement") 147 | 148 | let keys = Keys.sequence(.a, .b, .c) 149 | mockWebDriver.expect(path: "session/mySession/keys", method: .post, type: Requests.SessionKeys.self) { 150 | XCTAssertEqual($0.value.first, keys.rawValue) 151 | return CodableNone() 152 | } 153 | try session.sendKeys(keys, releaseModifiers: false) 154 | 155 | mockWebDriver.expect(path: "session/mySession/element/myElement/value", method: .post, type: Requests.ElementValue.self) { 156 | XCTAssertEqual($0.value.first, keys.rawValue) 157 | return CodableNone() 158 | } 159 | try element.sendKeys(keys) 160 | } 161 | 162 | func testElementText() throws { 163 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 164 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 165 | let element = Element(session: session, id: "myElement") 166 | mockWebDriver.expect(path: "session/mySession/element/myElement/text", method: .get) { 167 | ResponseWithValue("myElement.text") 168 | } 169 | XCTAssertEqual(try element.text, "myElement.text") 170 | } 171 | 172 | func testElementAttribute() throws { 173 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 174 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 175 | let element = Element(session: session, id: "myElement") 176 | mockWebDriver.expect(path: "session/mySession/element/myElement/attribute/myAttribute.name", method: .get) { 177 | ResponseWithValue("myAttribute.value") 178 | } 179 | XCTAssertEqual(try element.getAttribute(name: "myAttribute.name"), "myAttribute.value") 180 | } 181 | 182 | func testElementClick() throws { 183 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 184 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 185 | let element = Element(session: session, id: "myElement") 186 | mockWebDriver.expect(path: "session/mySession/element/myElement/click", method: .post) 187 | try element.click() 188 | } 189 | 190 | func testElementLocationAndSize() throws { 191 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 192 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 193 | let element = Element(session: session, id: "myElement") 194 | mockWebDriver.expect(path: "session/mySession/element/myElement/location", method: .get, type: Requests.ElementLocation.self) { 195 | ResponseWithValue(.init(x: 10, y: -20)) 196 | } 197 | XCTAssert(try element.location == (x: 10, y: -20)) 198 | 199 | mockWebDriver.expect(path: "session/mySession/element/myElement/size", method: .get, type: Requests.ElementSize.self) { 200 | ResponseWithValue(.init(width: 100, height: 200)) 201 | } 202 | XCTAssert(try element.size == (width: 100, height: 200)) 203 | } 204 | 205 | func testElementEnabled() throws { 206 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 207 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 208 | let element = Element(session: session, id: "myElement") 209 | mockWebDriver.expect(path: "session/mySession/element/myElement/enabled", method: .get) { 210 | ResponseWithValue(true) 211 | } 212 | XCTAssert(try element.enabled == true) 213 | } 214 | 215 | func testElementSelected() throws { 216 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 217 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 218 | let element = Element(session: session, id: "myElement") 219 | mockWebDriver.expect(path: "session/mySession/element/myElement/selected", method: .get) { 220 | ResponseWithValue(true) 221 | } 222 | XCTAssert(try element.selected == true) 223 | } 224 | 225 | func testWindowPosition() throws { 226 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 227 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 228 | mockWebDriver.expect(path: "session/mySession/window/myWindow/position", method: .post) 229 | try session.window(handle: "myWindow").setPosition(x: 9, y: 16) 230 | 231 | mockWebDriver.expect(path: "session/mySession/window/myWindow/position", method: .get, type: Requests.WindowPosition.Get.self) { 232 | ResponseWithValue(.init(x: 9, y: 16)) 233 | } 234 | XCTAssert(try session.window(handle: "myWindow").position == (x: 9, y: 16)) 235 | } 236 | 237 | func testSessionScript() throws { 238 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 239 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 240 | mockWebDriver.expect(path: "session/mySession/execute", method: .post) 241 | XCTAssertNotNil(try session.execute(script: "return document.body", args: ["script"], async: false)) 242 | } 243 | 244 | func testSessionScriptAsync() throws { 245 | let mockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 246 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 247 | mockWebDriver.expect(path: "session/mySession/execute_async", method: .post) 248 | XCTAssertNotNil(try session.execute(script: "return document.body", args: ["script"], async: true)) 249 | } 250 | 251 | func testSessionTouchScroll() throws { 252 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 253 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 254 | let element = Element(session: session, id: "myElement") 255 | mockWebDriver.expect(path: "session/mySession/touch/scroll", method: .post) 256 | try session.touchScroll(element: element, xOffset: 9, yOffset: 16) 257 | } 258 | 259 | func testWindow() throws { 260 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 261 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 262 | mockWebDriver.expect(path: "session/mySession/window", method: .post) 263 | try session.focus(window: "myWindow") 264 | 265 | mockWebDriver.expect(path: "session/mySession/window", method: .delete) 266 | try session.close(window: "myWindow") 267 | } 268 | 269 | func testWindowHandleSize() throws { 270 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 271 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 272 | mockWebDriver.expect(path: "session/mySession/window/myWindow/size", method: .post) 273 | try session.window(handle: "myWindow").setSize(width: 500, height: 500) 274 | 275 | mockWebDriver.expect(path: "session/mySession/window/myWindow/size", method: .get, type: Requests.WindowSize.Get.self) { 276 | ResponseWithValue(.init(width: 500, height: 500)) 277 | } 278 | XCTAssert(try session.window(handle: "myWindow").size == (width: 500, height: 500)) 279 | } 280 | 281 | func testLocation() throws { 282 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 283 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 284 | let location = Location(latitude: 5, longitude: 20, altitude: 2003) 285 | 286 | mockWebDriver.expect(path: "session/mySession/location", method: .post) 287 | try session.setLocation(location) 288 | 289 | mockWebDriver.expect(path: "session/mySession/location", method: .get, type: Requests.SessionLocation.Get.self) { 290 | ResponseWithValue(.init(latitude: 5, longitude: 20, altitude: 2003)) 291 | } 292 | XCTAssert(try session.location == location) 293 | } 294 | 295 | func testMaximizeWindow() throws { 296 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 297 | let session: Session = Session(webDriver: mockWebDriver, existingId: "mySession") 298 | mockWebDriver.expect(path: "session/mySession/window/myWindow/maximize", method: .post) 299 | try session.window(handle: "myWindow").maximize() 300 | } 301 | 302 | func testWindowHandle() throws { 303 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 304 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 305 | 306 | mockWebDriver.expect(path: "session/mySession/window_handle", method: .get, type: Requests.SessionWindowHandle.self) { 307 | ResponseWithValue(.init("myWindow")) 308 | } 309 | XCTAssert(try session.windowHandle == "myWindow") 310 | } 311 | 312 | func testWindowHandles() throws { 313 | 314 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 315 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 316 | 317 | mockWebDriver.expect(path: "session/mySession/window_handles", method: .get, type: Requests.SessionWindowHandles.self) { 318 | ResponseWithValue(.init(["myWindow", "myWindow"])) 319 | } 320 | XCTAssert(try session.windowHandles == ["myWindow", "myWindow"]) 321 | } 322 | 323 | 324 | func testElementDoubleClick() throws { 325 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 326 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 327 | let element = Element(session: session, id: "myElement") 328 | mockWebDriver.expect(path: "session/mySession/touch/doubleclick", method: .post) 329 | XCTAssertNotNil(try element.doubleClick()) 330 | } 331 | 332 | func testElementFlick() throws { 333 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 334 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 335 | let element = Element(session: session, id: "myElement") 336 | mockWebDriver.expect(path: "session/mySession/touch/flick", method: .post) 337 | XCTAssertNotNil(try element.flick(xOffset: 5, yOffset: 20, speed: 2003)) 338 | } 339 | 340 | func testSessionFlick() throws { 341 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 342 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 343 | mockWebDriver.expect(path: "session/mySession/touch/flick", method: .post) 344 | XCTAssertNotNil(try session.flick(xSpeed: 5, ySpeed: 20)) 345 | } 346 | 347 | func testSessionSource() throws { 348 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 349 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 350 | mockWebDriver.expect(path: "session/mySession/source", method: .get, type: Requests.SessionSource.self) { 351 | ResponseWithValue("currentSource") 352 | } 353 | XCTAssert(try session.source == "currentSource") 354 | } 355 | 356 | func testSessionTimeouts() throws { 357 | let mockWebDriver: MockWebDriver = MockWebDriver(wireProtocol: .legacySelenium) 358 | let session = Session(webDriver: mockWebDriver, existingId: "mySession") 359 | mockWebDriver.expect(path: "session/mySession/timeouts", method: .post) 360 | try session.setTimeout(type: .implicitWait, duration: 5) 361 | XCTAssert(session.implicitWaitTimeout == 5.0) 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /Tests/UnitTests/MockWebDriver.swift: -------------------------------------------------------------------------------- 1 | @testable import WebDriver 2 | import XCTest 3 | 4 | /// A mock WebDriver implementation which can be configured 5 | /// to expect certain requests and fail if they don't match. 6 | class MockWebDriver: WebDriver { 7 | struct UnexpectedRequestBodyError: Error {} 8 | 9 | struct Expectation { 10 | let path: String 11 | let method: HTTPMethod 12 | let handler: (Data?) throws -> Data? 13 | } 14 | 15 | let wireProtocol: WireProtocol 16 | var expectations: [Expectation] = [] 17 | 18 | public init(wireProtocol: WireProtocol) { 19 | self.wireProtocol = wireProtocol 20 | } 21 | 22 | deinit { 23 | // We should have met all of our expectations. 24 | XCTAssertEqual(expectations.count, 0) 25 | } 26 | 27 | /// Queues an expected request and specifies its response handler. 28 | /// This overload is the most generic for any incoming body type and outgoing response type. 29 | func expect(path: String, method: HTTPMethod, handler: @escaping (ReqBody) throws -> Res) { 30 | expectations.append(Expectation(path: path, method: method, handler: { 31 | let requestBody: ReqBody 32 | if let requestBodyData = $0 { 33 | requestBody = try JSONDecoder().decode(ReqBody.self, from: requestBodyData) 34 | } else if ReqBody.self == CodableNone.self { 35 | requestBody = CodableNone() as Any as! ReqBody 36 | } else { 37 | throw UnexpectedRequestBodyError() 38 | } 39 | 40 | let response = try handler(requestBody) 41 | return Res.self == CodableNone.self ? nil : try JSONEncoder().encode(response) 42 | })) 43 | } 44 | 45 | /// Queues an expected request and specifies its response handler. 46 | /// This overload uses a Request.Type for easier type inference. 47 | func expect(path: String, method: HTTPMethod, type: Req.Type, handler: @escaping (Req.Body) throws -> Req.Response) { 48 | expect(path: path, method: method) { 49 | (requestBody: Req.Body) -> Req.Response in try handler(requestBody) 50 | } 51 | } 52 | 53 | /// Queues an expected request and specifies its response handler and outoing response type. 54 | /// This overload ignores the incoming request body. 55 | func expect(path: String, method: HTTPMethod, type: Req.Type, handler: @escaping () throws -> Req.Response) { 56 | expect(path: path, method: method) { 57 | () -> Req.Response in try handler() 58 | } 59 | } 60 | 61 | /// Queues an expected request and specifies its response handler. 62 | /// This overload ignores the incoming request body. 63 | func expect(path: String, method: HTTPMethod, handler: @escaping () throws -> Res) { 64 | expect(path: path, method: method) { (_: CodableNone) -> Res in try handler() } 65 | } 66 | 67 | /// Queues an expected request 68 | /// This overload ignores the incoming request body and returns a default response. 69 | func expect(path: String, method: HTTPMethod) { 70 | expect(path: path, method: method) { 71 | (_: CodableNone) in CodableNone() 72 | } 73 | } 74 | 75 | @discardableResult 76 | func send(_ request: Req) throws -> Req.Response { 77 | XCTAssertNotEqual(expectations.count, 0) 78 | 79 | let expectation = expectations.remove(at: 0) 80 | XCTAssertEqual(request.pathComponents.joined(separator: "/"), expectation.path) 81 | XCTAssertEqual(request.method, expectation.method) 82 | 83 | let requestBody: Data? = Req.Body.self == CodableNone.self 84 | ? nil : try JSONEncoder().encode(request.body) 85 | 86 | let responseData = try expectation.handler(requestBody) 87 | if Req.Response.self == CodableNone.self { 88 | return CodableNone() as! Req.Response 89 | } else { 90 | return try JSONDecoder().decode(Req.Response.self, from: responseData!) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tests/WinAppDriverTests/AppDriverOptionsTest.swift: -------------------------------------------------------------------------------- 1 | import TestsCommon 2 | import WinSDK 3 | import XCTest 4 | 5 | @testable import WebDriver 6 | @testable import WinAppDriver 7 | 8 | class AppDriverOptionsTest: XCTestCase { 9 | func tempFileName() -> String { 10 | return FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) 11 | .appendingPathExtension("txt").path 12 | } 13 | 14 | /// Tests that redirecting stdout to a file works. 15 | func testStdoutRedirectToFile() throws { 16 | // Start a new instance of msinfo32 and write the output to a file. 17 | let outputFile = tempFileName() 18 | let _ = try MSInfo32App( 19 | winAppDriver: WinAppDriver.start( 20 | outputFile: outputFile 21 | )) 22 | 23 | // Read the output file. 24 | let output = try String(contentsOfFile: outputFile, encoding: .utf16LittleEndian) 25 | 26 | // Delete the file. 27 | try FileManager.default.removeItem(atPath: outputFile) 28 | 29 | XCTAssert(output.contains("msinfo32")) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/WinAppDriverTests/CommandLineTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WinAppDriver 2 | import XCTest 3 | 4 | class CommandLineTests: XCTestCase { 5 | func testbuildArgsString() { 6 | XCTAssertEqual(buildCommandLineArgsString(args: ["my dir\\file.txt"]), "\"my dir\\\\file.txt\"") 7 | XCTAssertEqual(buildCommandLineArgsString(args: ["my\tdir\\file.txt"]), "\"my\tdir\\\\file.txt\"") 8 | XCTAssertEqual(buildCommandLineArgsString(args: ["-m:\"description\""]), "-m:\\\"description\\\"") 9 | XCTAssertEqual(buildCommandLineArgsString(args: ["-m:\"commit description\""]), "\"-m:\\\"commit description\\\"\"") 10 | XCTAssertEqual(buildCommandLineArgsString(args: ["m:\"commit description\"", "my dir\\file.txt "]), "\"m:\\\"commit description\\\"\" \"my dir\\\\file.txt \"") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/WinAppDriverTests/MSInfo32App.swift: -------------------------------------------------------------------------------- 1 | @testable import WinAppDriver 2 | import XCTest 3 | 4 | class MSInfo32App { 5 | static let findWhatEditBoxAccelerator = Keys.alt(.w) 6 | static let searchSelectedCategoryOnlyCheckboxAccelerator = Keys.alt(.s) 7 | 8 | let session: Session 9 | 10 | init(winAppDriver: WinAppDriver) throws { 11 | let capabilities = WinAppDriver.Capabilities.startApp(name: "\(WindowsSystemPaths.system32)\\msinfo32.exe") 12 | session = try Session(webDriver: winAppDriver, capabilities: capabilities) 13 | session.implicitWaitTimeout = 1 14 | } 15 | 16 | private lazy var _maximizeButton = Result { 17 | try session.findElement(locator: .name("Maximize")) 18 | } 19 | var maximizeButton: Element { 20 | get throws { try _maximizeButton.get() } 21 | } 22 | 23 | private lazy var _systemSummaryTree = Result { 24 | try session.findElement(locator: .accessibilityId("201")) 25 | } 26 | var systemSummaryTree: Element { 27 | get throws { try _systemSummaryTree.get() } 28 | } 29 | 30 | private lazy var _findWhatEditBox = Result { 31 | try session.findElement(locator: .accessibilityId("204")) 32 | } 33 | var findWhatEditBox: Element { 34 | get throws { try _findWhatEditBox.get() } 35 | } 36 | 37 | private lazy var _searchSelectedCategoryOnlyCheckbox = Result { 38 | try session.findElement(locator: .accessibilityId("206")) 39 | } 40 | var searchSelectedCategoryOnlyCheckbox: Element { 41 | get throws { try _searchSelectedCategoryOnlyCheckbox.get() } 42 | } 43 | 44 | private lazy var _listView = Result { 45 | let elements = try XCTUnwrap(session.findElements(locator: .accessibilityId("202")), "List view not found") 46 | try XCTSkipIf(elements.count != 1, "Expected exactly one list view; request timeout?") 47 | return elements[0] 48 | } 49 | var listView: Element { 50 | get throws { try _listView.get() } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/WinAppDriverTests/RequestsTests.swift: -------------------------------------------------------------------------------- 1 | import TestsCommon 2 | @testable import WebDriver 3 | @testable import WinAppDriver 4 | import XCTest 5 | 6 | class RequestsTests: XCTestCase { 7 | static var winAppDriver: Result! 8 | 9 | override class func setUp() { 10 | winAppDriver = Result { try WinAppDriver.start() } 11 | } 12 | 13 | override class func tearDown() { 14 | winAppDriver = nil 15 | } 16 | 17 | var app: MSInfo32App! 18 | 19 | override func setUpWithError() throws { 20 | app = try MSInfo32App(winAppDriver: Self.winAppDriver.get()) 21 | } 22 | 23 | override func tearDown() { 24 | app = nil 25 | } 26 | 27 | func testCanGetChildElements() throws { 28 | let children = try XCTUnwrap(app.listView.findElements(locator: .xpath("//ListItem"))) 29 | XCTAssert(children.count > 0) 30 | } 31 | 32 | func testStatusReportsWinAppDriverOnWindows() throws { 33 | let status = try XCTUnwrap(app.session.webDriver.status) 34 | XCTAssertNotNil(status.build?.version) 35 | XCTAssert(status.os?.name == "windows") 36 | } 37 | 38 | func testSessionTitleReadsWindowTitle() throws { 39 | XCTAssertEqual(try app.session.title, "System Information") 40 | } 41 | 42 | func testElementSizeCanChange() throws { 43 | let oldSize = try app.systemSummaryTree.size 44 | try app.maximizeButton.click() 45 | let newSize = try app.systemSummaryTree.size 46 | try app.maximizeButton.click() 47 | XCTAssertNotEqual(oldSize.width, newSize.width) 48 | XCTAssertNotEqual(oldSize.height, newSize.height) 49 | } 50 | 51 | func testScreenshotReturnsPNG() throws { 52 | XCTAssert(isPNG(data: try app.session.screenshot())) 53 | } 54 | 55 | func testAttributes() throws { 56 | try XCTAssertEqual(app.findWhatEditBox.getAttribute(name: WinAppDriver.Attributes.className), "Edit") 57 | } 58 | 59 | func testEnabled() throws { 60 | try XCTAssert(app.findWhatEditBox.enabled) 61 | } 62 | 63 | func testElementClickGivesKeyboardFocus() throws { 64 | try app.systemSummaryTree.click() 65 | try XCTAssert(!Self.hasKeyboardFocus(app.findWhatEditBox)) 66 | try app.findWhatEditBox.click() 67 | try XCTAssert(Self.hasKeyboardFocus(app.findWhatEditBox)) 68 | } 69 | 70 | func testMouseMoveToElementPositionsCursorAccordingly() throws { 71 | try app.systemSummaryTree.click() 72 | try XCTAssert(!Self.hasKeyboardFocus(app.findWhatEditBox)) 73 | let size = try app.findWhatEditBox.size 74 | try app.session.moveTo(element: app.findWhatEditBox, xOffset: size.width / 2, yOffset: size.height / 2) 75 | try app.session.click() 76 | try XCTAssert(Self.hasKeyboardFocus(app.findWhatEditBox)) 77 | } 78 | 79 | func testSendKeysWithUnicodeCharacter() throws { 80 | // k: Requires no modifiers on a US Keyboard 81 | // K: Requires modifiers on a US Keyboard 82 | // ł: Not typeable on a US Keyboard 83 | // ☃: Unicode BMP character 84 | let str = "kKł☃" 85 | try app.findWhatEditBox.sendKeys(.text(str, typingStrategy: .windowsKeyboardAgnostic)) 86 | 87 | // Normally we should be able to read the text back immediately, 88 | // but the MSInfo32 "Find what" edit box seems to queue events 89 | // such that WinAppDriver returns before they are fully processed. 90 | struct UnexpectedText: Error { var text: String } 91 | _ = try poll(timeout: 0.5) { 92 | let text = try app.findWhatEditBox.text 93 | return text == str ? .success(()) : .failure(UnexpectedText(text: text)) 94 | } 95 | } 96 | 97 | func testSendKeysWithAcceleratorsGivesFocus() throws { 98 | try app.session.sendKeys(MSInfo32App.findWhatEditBoxAccelerator) 99 | try XCTAssert(Self.hasKeyboardFocus(app.findWhatEditBox)) 100 | try app.session.sendKeys(.tab) 101 | try XCTAssert(!Self.hasKeyboardFocus(app.findWhatEditBox)) 102 | } 103 | 104 | func testSessionSendKeys_scopedModifiers() throws { 105 | try app.findWhatEditBox.click() 106 | try app.session.sendKeys(.sequence(.shift(.a), .a)) 107 | XCTAssertEqual(try app.findWhatEditBox.text, "Aa") 108 | } 109 | 110 | func testSessionSendKeys_autoReleasedModifiers() throws { 111 | try app.findWhatEditBox.click() 112 | try app.session.sendKeys(.sequence(.shiftModifier, .a)) 113 | try app.session.sendKeys(.a) 114 | XCTAssertEqual(try app.findWhatEditBox.text, "Aa") 115 | } 116 | 117 | func testSessionSendKeys_stickyModifiers() throws { 118 | try app.findWhatEditBox.click() 119 | try app.session.sendKeys(.sequence(.shiftModifier, .a), releaseModifiers: false) 120 | try app.session.sendKeys(.a) 121 | try app.session.sendKeys(.releaseModifiers) 122 | try app.session.sendKeys(.a) 123 | XCTAssertEqual(try app.findWhatEditBox.text, "AAa") 124 | } 125 | 126 | func testElementSendKeys_scopedModifiers() throws { 127 | try app.findWhatEditBox.sendKeys(.sequence(.shift(.a), .a)) 128 | XCTAssertEqual(try app.findWhatEditBox.text, "Aa") 129 | } 130 | 131 | func testElementSendKeys_autoReleasedModifiers() throws { 132 | try app.findWhatEditBox.sendKeys(.sequence(.shiftModifier, .a)) 133 | try app.findWhatEditBox.sendKeys(.a) 134 | XCTAssertEqual(try app.findWhatEditBox.text, "Aa") 135 | } 136 | 137 | private static func hasKeyboardFocus(_ element: Element) throws -> Bool { 138 | try XCTUnwrap(element.getAttribute(name: WinAppDriver.Attributes.hasKeyboardFocus)).lowercased() == "true" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/WinAppDriverTests/TimeoutTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WinAppDriver 2 | @testable import WebDriver 3 | import XCTest 4 | 5 | class TimeoutTests: XCTestCase { 6 | private static var _winAppDriver: Result! 7 | var winAppDriver: WinAppDriver! { try? Self._winAppDriver.get() } 8 | 9 | override class func setUp() { 10 | _winAppDriver = Result { try WinAppDriver.start() } 11 | } 12 | 13 | override class func tearDown() { 14 | _winAppDriver = nil 15 | } 16 | 17 | override func setUpWithError() throws { 18 | if case .failure(let error) = Self._winAppDriver { 19 | throw XCTSkip("Failed to start WinAppDriver: \(error)") 20 | } 21 | } 22 | 23 | func startApp() throws -> Session { 24 | // Use a simple app in which we can expect queries to execute quickly 25 | let capabilities = WinAppDriver.Capabilities.startApp( 26 | name: "\(WindowsSystemPaths.system32)\\winver.exe") 27 | return try Session(webDriver: winAppDriver, capabilities: capabilities) 28 | } 29 | 30 | static func measureTime(_ callback: () throws -> Void) rethrows -> Double { 31 | let before = DispatchTime.now() 32 | try callback() 33 | let after = DispatchTime.now() 34 | return Double(after.uptimeNanoseconds - before.uptimeNanoseconds) / 1_000_000_000 35 | } 36 | 37 | static func measureNoSuchElementTime(_ session: Session) -> Double { 38 | measureTime { 39 | do { 40 | try session.findElement(locator: .accessibilityId("IdThatDoesNotExist")) 41 | XCTFail("Expected a no such element error") 42 | } 43 | catch {} 44 | } 45 | } 46 | 47 | public func testWebDriverImplicitWait() throws { 48 | let session = try startApp() 49 | 50 | session.implicitWaitTimeout = 1 51 | XCTAssertGreaterThan(Self.measureNoSuchElementTime(session), 0.5) 52 | 53 | session.implicitWaitTimeout = 0 54 | XCTAssertLessThan(Self.measureNoSuchElementTime(session), 0.5) 55 | 56 | XCTAssert(!session.emulateImplicitWait) 57 | } 58 | 59 | public func testEmulatedImplicitWait() throws { 60 | let session = try startApp() 61 | 62 | // Test library timeout implementation 63 | session.emulateImplicitWait = true 64 | session.implicitWaitTimeout = 1 65 | XCTAssertGreaterThan(Self.measureNoSuchElementTime(session), 0.5) 66 | 67 | session.implicitWaitTimeout = 0 68 | XCTAssertLessThan(Self.measureNoSuchElementTime(session), 0.5) 69 | } 70 | } 71 | --------------------------------------------------------------------------------