├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ └── pkgdown.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── actions.R ├── capabilities.R ├── commands.R ├── element.R ├── import-standalone-obj-type.R ├── import-standalone-types-check.R ├── json.R ├── keys.R ├── selenium-package.R ├── server.R ├── session.R ├── status.R ├── sysdata.rda ├── utils-checks.R └── utils.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codemeta.json ├── cran-comments.md ├── data-raw └── internal.R ├── man ├── SeleniumServer.Rd ├── SeleniumSession.Rd ├── ShadowRoot.Rd ├── WebElement.Rd ├── actions_mousedown.Rd ├── actions_pause.Rd ├── actions_press.Rd ├── actions_scroll.Rd ├── actions_stream.Rd ├── chrome_options.Rd ├── figures │ ├── lifecycle-archived.svg │ ├── lifecycle-defunct.svg │ ├── lifecycle-deprecated.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-maturing.svg │ ├── lifecycle-questioning.svg │ ├── lifecycle-soft-deprecated.svg │ ├── lifecycle-stable.svg │ └── lifecycle-superseded.svg ├── key_chord.Rd ├── keys.Rd ├── selenium-package.Rd ├── selenium_server.Rd └── wait_for_server.Rd ├── revdep ├── .gitignore ├── README.md ├── cran.md ├── failures.md └── problems.md ├── tests ├── testthat.R └── testthat │ ├── helper-session.R │ ├── helper-site.html │ ├── test-element.R │ ├── test-keys.R │ ├── test-server.R │ └── test-session.R └── vignettes ├── .gitignore └── articles ├── debugging.Rmd └── selenium.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^data-raw$ 2 | ^LICENSE\.md$ 3 | ^README\.Rmd$ 4 | ^_pkgdown\.yml$ 5 | ^docs$ 6 | ^pkgdown$ 7 | ^\.github$ 8 | ^codecov\.yml$ 9 | ^vignettes/articles$ 10 | ^cran-comments\.md$ 11 | ^codemeta\.json$ 12 | ^CRAN-SUBMISSION$ 13 | ^revdep$ 14 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.browser }}, ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 22 | - {os: ubuntu-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'oldrel-1'} 24 | browser: [chrome, firefox] 25 | 26 | services: 27 | selenium: 28 | image: selenium/standalone-${{ matrix.browser }}:4.14.1-20231020 29 | ports: 30 | - 4444:4444 31 | options: >- 32 | --shm-size="2g" 33 | 34 | env: 35 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 36 | R_KEEP_PKG_SOURCE: yes 37 | SELENIUM_BROWSER: ${{ matrix.browser }} 38 | 39 | steps: 40 | - uses: actions/checkout@v3 41 | 42 | - uses: r-lib/actions/setup-pandoc@v2 43 | 44 | - uses: r-lib/actions/setup-r@v2 45 | with: 46 | r-version: ${{ matrix.config.r }} 47 | http-user-agent: ${{ matrix.config.http-user-agent }} 48 | use-public-rspm: true 49 | 50 | - uses: r-lib/actions/setup-r-dependencies@v2 51 | with: 52 | extra-packages: any::rcmdcheck 53 | needs: check 54 | 55 | - uses: r-lib/actions/check-r-package@v2 56 | with: 57 | upload-snapshots: true 58 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | 25 | services: 26 | selenium: 27 | image: selenium/standalone-firefox:4.14.1-20231020 28 | ports: 29 | - 4444:4444 30 | options: >- 31 | --shm-size="2g" 32 | 33 | steps: 34 | - uses: actions/checkout@v3 35 | 36 | - uses: r-lib/actions/setup-pandoc@v2 37 | 38 | - uses: r-lib/actions/setup-r@v2 39 | with: 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::pkgdown, local::. 45 | needs: website 46 | 47 | - name: Build site 48 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 49 | shell: Rscript {0} 50 | 51 | - name: Deploy to GitHub pages 🚀 52 | if: github.event_name != 'pull_request' 53 | uses: JamesIves/github-pages-deploy-action@v4.4.1 54 | with: 55 | clean: false 56 | branch: gh-pages 57 | folder: docs 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .Rdata 4 | .httr-oauth 5 | .DS_Store 6 | .quarto 7 | docs 8 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: selenium 2 | Title: Low-Level Browser Automation Interface 3 | Version: 0.1.4.9000 4 | Authors@R: 5 | person("Ashby", "Thorpe", , "ashbythorpe@gmail.com", role = c("aut", "cre", "cph"), 6 | comment = c(ORCID = "0000-0003-3106-099X")) 7 | Description: An implementation of 'W3C WebDriver 2.0' 8 | (), allowing interaction 9 | with a 'Selenium Server' () 10 | instance from 'R'. Allows a web browser to be automated from 'R'. 11 | License: MIT + file LICENSE 12 | Encoding: UTF-8 13 | Roxygen: list(markdown = TRUE) 14 | RoxygenNote: 7.3.2 15 | Depends: 16 | R (>= 2.10) 17 | Suggests: 18 | gitcreds, 19 | testthat (>= 3.0.0), 20 | withr, 21 | xml2 22 | Config/testthat/edition: 3 23 | Imports: 24 | base64enc, 25 | httr2, 26 | jsonlite, 27 | lifecycle, 28 | processx, 29 | R6, 30 | rappdirs, 31 | rlang (>= 1.1.0) 32 | URL: https://ashbythorpe.github.io/selenium-r/, https://github.com/ashbythorpe/selenium-r 33 | Config/Needs/website: rmarkdown 34 | BugReports: https://github.com/ashbythorpe/selenium-r/issues 35 | Language: en-GB 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: selenium authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 selenium authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(SeleniumServer) 4 | export(SeleniumSession) 5 | export(ShadowRoot) 6 | export(WebElement) 7 | export(actions_mousedown) 8 | export(actions_mousemove) 9 | export(actions_mouseup) 10 | export(actions_pause) 11 | export(actions_press) 12 | export(actions_release) 13 | export(actions_scroll) 14 | export(actions_stream) 15 | export(chrome_options) 16 | export(edge_options) 17 | export(firefox_options) 18 | export(get_server_status) 19 | export(key_chord) 20 | export(keys) 21 | export(selenium_server) 22 | export(selenium_server_available) 23 | export(wait_for_selenium_available) 24 | export(wait_for_server) 25 | import(rlang) 26 | importFrom(R6,is.R6Class) 27 | importFrom(lifecycle,deprecated) 28 | importFrom(processx,process) 29 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # selenium (development version) 2 | 3 | - Respect `JAVA_HOME` when finding the `java` executable. 4 | - `selenium_server()` now returns a `SeleniumServer` object, which exposes the 5 | `host` and `port` fields. This allows `wait_for_server()` to work without 6 | having to pass in the `host` and `port` arguments. 7 | - `selenium_server()` no longer saves the logs of the Selenium server process 8 | by default, as this would cause errors when creating lots of sessions. To 9 | enable logging, set the `stdout` and `stderr` arguments to `"|"`, and then 10 | use `$read_output()` and `$read_error()` to read the output/error, as before. 11 | 12 | # selenium 0.1.4 13 | 14 | - Added `chrome_options()`, `firefox_options()` and `edge_options()` to help 15 | with the `capabilities` argument in `SeleniumSession$new()`. The documentation 16 | there includes several links that document the options available for each 17 | browser, along with a few examples. 18 | - The `error` argument in `wait_for_selenium_available()` now defaults to 19 | `TRUE`. This means that the function will throw an error if a Selenium server 20 | is not available by default. 21 | - Added `wait_for_server()`, a version of `wait_for_selenium_available()` that 22 | gives more detailed error messages by reading the logs of a server process 23 | created using `selenium_server()`. 24 | - Fixed a bug in `selenium_server()` where fetching the latest version 25 | didn't work when Selenium was preparing to release a new version. 26 | 27 | # selenium 0.1.3 28 | 29 | - The `browser`, `host` and `port` fields can now be used to access the browser, 30 | host and port of a `SeleniumSession` object. 31 | - All web requests now have a 20 second timeout by default, meaning that if a 32 | response is not received within 20 seconds, an error is thrown. This stops 33 | requests running indefinitely, and can be customised with the `timeout` 34 | argument to each method. 35 | - Previously, `selenium_server()` could error if called too many times with 36 | `version = "latest"`. This is because a GitHub request is made to access 37 | the latest version, which can cause GitHub's rate limits to be exceeded. 38 | This update takes two steps to stop this from happening: 39 | - The latest version, when fetched, is cached in the current R session, 40 | allowing it to be re-used. 41 | - `gitcreds` is used, if available, to authenticate GitHub requests, 42 | increasing the rate limit. 43 | - `selenium_server()` passes in `"|"` to `stdout` and `stderr`, instead of 44 | exposing them as user arguments. This allows the output/error to be read 45 | using `$read_output()` and `$read_error()`. 46 | 47 | # selenium 0.1.2 48 | 49 | - Add `temp` argument to `selenium_server()`. 50 | 51 | # selenium 0.1.1 52 | 53 | - Added `path` argument to `selenium_server()`, allowing the file to be 54 | saved in a custom path. 55 | - Removed `\dontrun{}` for one example for CRAN resubmission. 56 | 57 | # selenium 0.1.0 58 | 59 | - Initial CRAN submission. 60 | -------------------------------------------------------------------------------- /R/actions.R: -------------------------------------------------------------------------------- 1 | #' Create a set of actions to be performed 2 | #' 3 | #' `actions_stream()` creates a set of actions to be performed by 4 | #' `SeleniumSession$perform_actions()`. Actions are a low level way to interact 5 | #' with a page. 6 | #' 7 | #' @param ... `selenium_action` objects: the actions to perform. 8 | #' 9 | #' @returns A `selenium_actions_stream` object, ready to be passed into 10 | #' `SeleniumSession$perform_actions()`. 11 | #' 12 | #' @seealso 13 | #' * Pause actions: [actions_pause()]. 14 | #' * Press actions: [actions_press()] and [actions_release()]. 15 | #' * Mouse actions: [actions_mousedown()], [actions_mouseup()] 16 | #' and [actions_mousemove()]. 17 | #' * Scroll actions: [actions_scroll()]. 18 | #' 19 | #' @examples 20 | #' actions_stream( 21 | #' actions_press(keys$enter), 22 | #' actions_pause(0.5), 23 | #' actions_release(keys$enter), 24 | #' actions_scroll(x = 1, y = 1, delta_x = 1, delta_y = 1, duration = 0.5), 25 | #' actions_mousemove(x = 1, y = 1, duration = 1, origin = "pointer") 26 | #' ) 27 | #' 28 | #' @export 29 | actions_stream <- function(...) { 30 | actions <- rlang::list2(...) 31 | actions_stream <- list() 32 | for (action in actions) { 33 | check_class(action, "selenium_action", arg = I("All items in `...`")) 34 | actions_stream <- append_action(actions_stream, action) 35 | } 36 | 37 | class(actions_stream) <- "selenium_actions_stream" 38 | actions_stream 39 | } 40 | 41 | append_action <- function(x, action) { 42 | stopifnot(inherits(action, "selenium_action")) 43 | current_sequence <- if (length(x) == 0) NULL else x[[length(x)]] 44 | sequence_type <- current_sequence$type 45 | current_type <- sequence_type(action) 46 | 47 | if (!is.null(sequence_type)) { 48 | sequence <- add_to_sequence(current_sequence, sequence_type, action, current_type) 49 | if (!is.null(sequence)) { 50 | x[[length(x)]] <- sequence 51 | return(x) 52 | } 53 | } 54 | 55 | new_sequence <- new_actions_sequence(action) 56 | append(x, list(new_sequence)) 57 | } 58 | 59 | add_to_sequence <- function(current_sequence, sequence_type, action, current_type) { 60 | if (sequence_type == current_type || current_type == "none") { 61 | current_sequence$actions <- append(current_sequence$actions, list(action)) 62 | } else if (sequence_type == "none") { 63 | current_sequence$actions <- append(current_sequence$actions, list(action)) 64 | current_sequence$type <- current_type 65 | } else { 66 | return(NULL) 67 | } 68 | 69 | current_sequence 70 | } 71 | 72 | new_actions_sequence <- function(first_action) { 73 | type <- sequence_type(first_action) 74 | list(type = type, id = rand_id(), actions = list(first_action)) 75 | } 76 | 77 | sequence_type <- function(action) { 78 | if (inherits(action, "selenium_action_pause")) { 79 | "none" 80 | } else if (inherits(action, "selenium_action_key")) { 81 | "key" 82 | } else if (inherits(action, "selenium_action_pointer")) { 83 | "pointer" 84 | } else { 85 | "wheel" 86 | } 87 | } 88 | 89 | #' Wait for a period of time 90 | #' 91 | #' A pause action to be passed into [actions_stream()]. Waits for a given 92 | #' number of seconds before performing the next action in the stream. 93 | #' 94 | #' @param seconds The number of seconds to wait for. 95 | #' 96 | #' @returns A `selenium_action` object. 97 | #' 98 | #' @examples 99 | #' actions_stream( 100 | #' actions_pause(1) 101 | #' ) 102 | #' 103 | #' @export 104 | actions_pause <- function(seconds) { 105 | check_number_decimal(seconds) 106 | 107 | data <- list( 108 | type = "pause", 109 | duration = seconds * 1000L 110 | ) 111 | 112 | class(data) <- c("selenium_action", "selenium_action_pause") 113 | data 114 | } 115 | 116 | #' Press or release a key 117 | #' 118 | #' Key actions to be passed into [actions_stream()]. `actions_press()` 119 | #' represents pressing a key on the keyboard, while `actions_release()` 120 | #' represents releasing a key. 121 | #' 122 | #' @param key The key to press: a string consisting of a single character. Use 123 | #' the [keys] object to use special keys (e.g. `Ctrl`). 124 | #' 125 | #' @returns A `selenium_action` object. 126 | #' 127 | #' @examples 128 | #' actions_stream( 129 | #' actions_press("a"), 130 | #' actions_release("a"), 131 | #' actions_press(keys$enter), 132 | #' actions_release(keys$enter) 133 | #' ) 134 | #' 135 | #' @export 136 | actions_press <- function(key) { 137 | check_char(key) 138 | 139 | data <- list( 140 | type = "keyDown", 141 | value = key 142 | ) 143 | 144 | class(data) <- c("selenium_action", "selenium_action_key", "selenium_action_press") 145 | data 146 | } 147 | 148 | #' @rdname actions_press 149 | #' 150 | #' @export 151 | actions_release <- function(key) { 152 | check_char(key) 153 | 154 | data <- list( 155 | type = "keyUp", 156 | value = key 157 | ) 158 | 159 | class(data) <- c("selenium_action", "selenium_action_key", "selenium_action_release") 160 | data 161 | } 162 | 163 | #' Press, release or move the mouse 164 | #' 165 | #' Mouse actions to be passed into [actions_stream()]. `actions_mousedown()` 166 | #' represents pressing a button on the mouse, while `actions_mouseup()` 167 | #' represents releasing a button. `actions_mousemove()` represents moving the 168 | #' mouse. 169 | #' 170 | #' @param button The mouse button to press. 171 | #' @param width The 'width' of the click, a number. 172 | #' @param height The 'height' of the click, a number. 173 | #' @param pressure The amount of pressure to apply to the click: a number 174 | #' between 0 and 1. 175 | #' @param tangential_pressure A number between 0 and 1. 176 | #' @param tilt_x A whole number between -90 and 90. 177 | #' @param tilt_y A whole number between -90 and 90. 178 | #' @param twist A whole number between 0 and 359. 179 | #' @param altitude_angle A number between 0 and `pi/2`. 180 | #' @param azimuth_angle A number between 0 and `2*pi`. 181 | #' 182 | #' @returns A `selenium_action` object. 183 | #' 184 | #' @examples 185 | #' actions_stream( 186 | #' actions_mousedown("left", width = 1, height = 1, pressure = 0.5), 187 | #' actions_mouseup("left", width = 100, height = 50, pressure = 1), 188 | #' actions_mousemove(x = 1, y = 1, duration = 1, origin = "pointer") 189 | #' ) 190 | #' 191 | #' @export 192 | actions_mousedown <- function(button = c("left", "right", "middle"), 193 | width = NULL, 194 | height = NULL, 195 | pressure = NULL, 196 | tangential_pressure = NULL, 197 | tilt_x = NULL, 198 | tilt_y = NULL, 199 | twist = NULL, 200 | altitude_angle = NULL, 201 | azimuth_angle = NULL) { 202 | button <- rlang::arg_match(button) 203 | button <- switch(button, 204 | "left" = 0, 205 | "middle" = 1, 206 | "right" = 2 207 | ) 208 | check_number_whole(width, min = 0, allow_null = TRUE) 209 | check_number_whole(height, min = 0, allow_null = TRUE) 210 | check_number_decimal(pressure, min = 0, max = 1, allow_null = TRUE) 211 | check_number_decimal(tangential_pressure, min = 0, max = 1, allow_null = TRUE) 212 | check_number_whole(tilt_x, min = -90, max = 90, allow_null = TRUE) 213 | check_number_whole(tilt_y, min = -90, max = 90, allow_null = TRUE) 214 | check_number_whole(twist, min = 0, max = 359, allow_null = TRUE) 215 | check_number_decimal(altitude_angle, min = 0, max = pi / 2, allow_null = TRUE) 216 | check_number_decimal(azimuth_angle, min = 0, max = 2 * pi, allow_null = TRUE) 217 | 218 | parameters <- compact(list( 219 | button = button, 220 | width = width, 221 | height = height, 222 | pressure = pressure, 223 | tangentialPressure = tangential_pressure, 224 | tiltX = tilt_x, 225 | tiltY = tilt_y, 226 | twist = twist, 227 | altitudeAngle = altitude_angle, 228 | azimuthAngle = azimuth_angle 229 | )) 230 | 231 | data <- rlang::list2( 232 | type = "pointerDown", 233 | !!!parameters 234 | ) 235 | 236 | class(data) <- c("selenium_action", "selenium_action_pointer", "selenium_action_pointer_down") 237 | data 238 | } 239 | 240 | #' @rdname actions_mousedown 241 | #' 242 | #' @export 243 | actions_mouseup <- function(button = c("left", "right", "middle"), 244 | width = NULL, 245 | height = NULL, 246 | pressure = NULL, 247 | tangential_pressure = NULL, 248 | tilt_x = NULL, 249 | tilt_y = NULL, 250 | twist = NULL, 251 | altitude_angle = NULL, 252 | azimuth_angle = NULL) { 253 | button <- rlang::arg_match(button) 254 | button <- switch(button, 255 | "left" = 0, 256 | "middle" = 1, 257 | "right" = 2 258 | ) 259 | check_number_whole(width, min = 0, allow_null = TRUE) 260 | check_number_whole(height, min = 0, allow_null = TRUE) 261 | check_number_decimal(pressure, min = 0, max = 1, allow_null = TRUE) 262 | check_number_decimal(tangential_pressure, min = 0, max = 1, allow_null = TRUE) 263 | check_number_whole(tilt_x, min = -90, max = 90, allow_null = TRUE) 264 | check_number_whole(tilt_y, min = -90, max = 90, allow_null = TRUE) 265 | check_number_whole(twist, min = 0, max = 359, allow_null = TRUE) 266 | check_number_decimal(altitude_angle, min = 0, max = pi / 2, allow_null = TRUE) 267 | check_number_decimal(azimuth_angle, min = 0, max = 2 * pi, allow_null = TRUE) 268 | 269 | parameters <- compact(list( 270 | button = button, 271 | width = width, 272 | height = height, 273 | pressure = pressure, 274 | tangentialPressure = tangential_pressure, 275 | tiltX = tilt_x, 276 | tiltY = tilt_y, 277 | twist = twist, 278 | altitudeAngle = altitude_angle, 279 | azimuthAngle = azimuth_angle 280 | )) 281 | 282 | data <- rlang::list2( 283 | type = "pointerUp", 284 | !!!parameters 285 | ) 286 | 287 | class(data) <- c("selenium_action", "selenium_action_pointer", "selenium_action_pointer_up") 288 | data 289 | } 290 | 291 | #' @rdname actions_mousedown 292 | #' 293 | #' @param x The x coordinate of the mouse movement. 294 | #' @param y The y coordinate of the mouse movement. 295 | #' @param duration The duration of the mouse movement, in seconds. 296 | #' @param origin The point from which `x` and `y` are measured. Can be a 297 | #' `WebElement` object, in which case `x` and `y` are measured from the 298 | #' center of the element. 299 | #' 300 | #' @export 301 | actions_mousemove <- function(x, 302 | y, 303 | duration = NULL, 304 | origin = c("viewport", "pointer"), 305 | width = NULL, 306 | height = NULL, 307 | pressure = NULL, 308 | tangential_pressure = NULL, 309 | tilt_x = NULL, 310 | tilt_y = NULL, 311 | twist = NULL, 312 | altitude_angle = NULL, 313 | azimuth_angle = NULL) { 314 | check_number_whole(x) 315 | check_number_whole(y) 316 | check_number_decimal(duration, min = 0, allow_null = TRUE) 317 | if (inherits(origin, "WebElement")) { 318 | origin <- origin$toJSON() 319 | } else { 320 | origin <- rlang::arg_match(origin) 321 | } 322 | check_number_whole(width, min = 0, allow_null = TRUE) 323 | check_number_whole(height, min = 0, allow_null = TRUE) 324 | check_number_decimal(pressure, min = 0, max = 1, allow_null = TRUE) 325 | check_number_decimal(tangential_pressure, min = 0, max = 1, allow_null = TRUE) 326 | check_number_whole(tilt_x, min = -90, max = 90, allow_null = TRUE) 327 | check_number_whole(tilt_y, min = -90, max = 90, allow_null = TRUE) 328 | check_number_whole(twist, min = 0, max = 359, allow_null = TRUE) 329 | check_number_decimal(altitude_angle, min = 0, max = pi / 2, allow_null = TRUE) 330 | check_number_decimal(azimuth_angle, min = 0, max = 2 * pi, allow_null = TRUE) 331 | 332 | parameters <- compact(list( 333 | x = x, 334 | y = y, 335 | duration = duration * 1000L, 336 | origin = origin, 337 | width = width, 338 | height = height, 339 | pressure = pressure, 340 | tangentialPressure = tangential_pressure, 341 | tiltX = tilt_x, 342 | tiltY = tilt_y, 343 | twist = twist, 344 | altitudeAngle = altitude_angle, 345 | azimuthAngle = azimuth_angle 346 | )) 347 | 348 | data <- rlang::list2( 349 | type = "pointerMove", 350 | !!!parameters 351 | ) 352 | 353 | class(data) <- c("selenium_action", "selenium_action_pointer", "selenium_action_pointer_move") 354 | data 355 | } 356 | 357 | #' Scroll the page 358 | #' 359 | #' Scroll actions to be passed into [actions_stream()]. Scroll the page in 360 | #' a given direction. 361 | #' 362 | #' @param x The x coordinate from which the scroll action originates from. 363 | #' @param y The y coordinate from which the scroll action originates from. 364 | #' @param delta_x The number of pixels to scroll in the x direction. 365 | #' @param delta_y The number of pixels to scroll in the y direction. 366 | #' @param duration The duration of the scroll, in seconds. 367 | #' @param origin The point from which `x` and `y` are measured. Can be a 368 | #' `WebElement` object, in which case `x` and `y` are measured from the 369 | #' center of the element. Otherwise, `origin` must be `"viewport"`. 370 | #' 371 | #' @returns A `selenium_action` object. 372 | #' 373 | #' @examples 374 | #' actions_stream( 375 | #' actions_scroll(x = 1, y = 1, delta_x = 1, delta_y = 1, duration = 0.5) 376 | #' ) 377 | #' 378 | #' @export 379 | actions_scroll <- function(x, 380 | y, 381 | delta_x, 382 | delta_y, 383 | duration = NULL, 384 | origin = "viewport") { 385 | check_number_whole(x) 386 | check_number_whole(y) 387 | check_number_whole(delta_x) 388 | check_number_whole(delta_y) 389 | check_number_decimal(duration, min = 0, allow_null = TRUE) 390 | if (inherits(origin, "WebElement")) { 391 | origin <- origin$toJSON() 392 | } else { 393 | origin <- rlang::arg_match(origin) 394 | } 395 | 396 | parameters <- compact(list( 397 | x = x, 398 | y = y, 399 | duration = duration * 1000L, 400 | origin = origin, 401 | deltaX = delta_x, 402 | deltaY = delta_y 403 | )) 404 | 405 | data <- rlang::list2( 406 | type = "scroll", 407 | !!!parameters 408 | ) 409 | 410 | class(data) <- c("selenium_action", "selenium_action_scroll") 411 | data 412 | } 413 | 414 | unclass_stream <- function(x) { 415 | lapply(x, unclass_actions) 416 | } 417 | 418 | unclass_actions <- function(x) { 419 | x$actions <- lapply(x$actions, unclass) 420 | x 421 | } 422 | -------------------------------------------------------------------------------- /R/capabilities.R: -------------------------------------------------------------------------------- 1 | #' Custom browser options 2 | #' 3 | #' Create browser options to pass into the `capabilities` argument of 4 | #' [SeleniumSession$new()][SeleniumSession]. 5 | #' 6 | #' @param binary Path to the browser binary. 7 | #' @param args A character vector of additional arguments to pass to the 8 | #' browser. 9 | #' @param extensions A character vector of paths to browser extension (`.crx`) 10 | #' files. These will be base64 encoded before being passed to the browser. If 11 | #' you have already encoded the extensions, you can pass them using [I()]. 12 | #' For Firefox, use a profile to load extensions. 13 | #' @param prefs A named list of preferences to set in the browser. 14 | #' @param profile Path to a Firefox profile directory. This will be base64 15 | #' encoded before being passed to the browser. 16 | #' @param ... Additional options to pass to the browser. 17 | #' 18 | #' @details 19 | #' These functions allow you to more easily translate between Selenium code in 20 | #' other languages (e.g. Java/Python) to R. For example, consider the following 21 | #' Java code, adapted from the the 22 | #' [Selenium documentation](https://www.selenium.dev/documentation/webdriver/browsers/chrome/) 23 | #' 24 | #' ```java 25 | #' ChromeOptions options = new ChromeOptions(); 26 | #' 27 | #' options.setBinary("/path/to/chrome"); 28 | #' options.addArguments("--headless", "--disable-gpu"); 29 | #' options.addExtensions("/path/to/extension.crx"); 30 | #' options.setExperimentalOption("excludeSwitches", List.of("disable-popup-blocking")); 31 | #' ``` 32 | #' 33 | #' This can be translated to R as follows: 34 | #' 35 | #' ```r 36 | #' chrome_options( 37 | #' binary = "/path/to/chrome", 38 | #' args = c("--headless", "--disable-gpu"), 39 | #' extensions = "/path/to/extension.crx", 40 | #' excludeSwitches = list("disable-popup-blocking") 41 | #' ) 42 | #' ``` 43 | #' 44 | #' You can combine these options with non-browser specific options simply using 45 | #' [c()]. 46 | #' 47 | #' Note that Microsoft Edge options are very similar to Chrome options, since 48 | #' it is based on Chromium. 49 | #' 50 | #' @returns A list of browser options, with Chrome options under the name 51 | #' `goog:chromeOptions`, Firefox options under `moz:firefoxOptions`, and Edge 52 | #' options under `ms:edgeOptions`. 53 | #' 54 | #' @seealso 55 | #' For more information and examples on Chrome options, see: 56 | #' 57 | #' 58 | #' For Firefox options: 59 | #' 60 | #' 61 | #' For other options that affect Firefox but are not under `mox:firefoxOptions`, 62 | #' see: 63 | #' 64 | #' 65 | #' For Edge options, see: 66 | #' 67 | #' 68 | #' @examples 69 | #' # Basic options objects 70 | #' chrome_options( 71 | #' binary = "/path/to/chrome", 72 | #' args = c("--headless", "--disable-gpu"), 73 | #' detatch = TRUE, # An additional option described in the link above. 74 | #' prefs = list( 75 | #' "profile.default_content_setting_values.notifications" = 2 76 | #' ) 77 | #' ) 78 | #' 79 | #' 80 | #' firefox_options(binary = "/path/to/firefox") 81 | #' 82 | #' edge_options(binary = "/path/to/edge") 83 | #' 84 | #' # Setting the user agent 85 | #' chrome_options(args = c("--user-agent=My User Agent")) 86 | #' 87 | #' edge_options(args = c("--user-agent=My User Agent")) 88 | #' 89 | #' firefox_options(prefs = list( 90 | #' "general.useragent.override" = "My User Agent" 91 | #' )) 92 | #' 93 | #' # Using a proxy server 94 | #' 95 | #' chrome_options(args = c("--proxy-server=HOST:PORT")) 96 | #' 97 | #' edge_options(args = c("--proxy-server=HOST:PORT")) 98 | #' 99 | #' PORT <- 1 100 | #' firefox_options(prefs = list( 101 | #' "network.proxy.type" = 1, 102 | #' "network.proxy.socks" = "HOST", 103 | #' "network.proxy.socks_port" = PORT, 104 | #' "network.proxy.socks_remote_dns" = FALSE 105 | #' )) 106 | #' 107 | #' # Combining with other options 108 | #' browser_options <- chrome_options(binary = "/path/to/chrome") 109 | #' 110 | #' c(browser_options, list(platformName = "Windows")) 111 | #' 112 | #' @export 113 | chrome_options <- function(binary = NULL, 114 | args = NULL, 115 | extensions = NULL, 116 | prefs = NULL, 117 | ...) { 118 | check_string(binary, allow_null = TRUE) 119 | check_character(args, allow_null = TRUE) 120 | check_character(extensions, allow_null = TRUE) 121 | check_list(prefs, allow_null = TRUE) 122 | 123 | extensions_encoded <- if (is.null(extensions)) { 124 | NULL 125 | } else if (inherits(extensions, "AsIs")) { 126 | as.list(extensions) 127 | } else { 128 | lapply(extensions, function(x) { 129 | base64enc::base64encode(what = x) 130 | }) 131 | } 132 | 133 | list( 134 | `goog:chromeOptions` = compact(list( 135 | binary = binary, 136 | args = as.list(args), 137 | extensions = extensions_encoded, 138 | prefs = prefs, 139 | ... 140 | )) 141 | ) 142 | } 143 | 144 | #' @rdname chrome_options 145 | #' 146 | #' @export 147 | firefox_options <- function(binary = NULL, 148 | args = NULL, 149 | profile = NULL, 150 | prefs = NULL, 151 | ...) { 152 | check_string(binary, allow_null = TRUE) 153 | check_character(args, allow_null = TRUE) 154 | check_string(profile, allow_null = TRUE) 155 | check_list(prefs, allow_null = TRUE) 156 | 157 | if (!is.null(profile) && !inherits(profile, "AsIs")) { 158 | profile <- base64enc::base64encode(what = profile) 159 | } 160 | 161 | list( 162 | acceptInsecureCerts = TRUE, 163 | `moz:firefoxOptions` = compact(list( 164 | binary = binary, 165 | args = as.list(args), 166 | profile = profile, 167 | prefs = prefs, 168 | ... 169 | )), 170 | `moz:debuggerAddress` = TRUE 171 | ) 172 | } 173 | 174 | #' @rdname chrome_options 175 | #' 176 | #' @export 177 | edge_options <- function(binary = NULL, 178 | args = NULL, 179 | extensions = NULL, 180 | prefs = NULL, 181 | ...) { 182 | check_string(binary, allow_null = TRUE) 183 | check_character(args, allow_null = TRUE) 184 | check_character(extensions, allow_null = TRUE) 185 | check_list(prefs, allow_null = TRUE) 186 | 187 | extensions_encoded <- if (is.null(extensions)) { 188 | NULL 189 | } else if (inherits(extensions, "AsIs")) { 190 | as.list(extensions) 191 | } else { 192 | lapply(extensions, function(x) { 193 | base64enc::base64encode(what = x) 194 | }) 195 | } 196 | 197 | list( 198 | `ms:edgeOptions` = compact(list( 199 | binary = binary, 200 | args = as.list(args), 201 | extensions = extensions_encoded, 202 | prefs = prefs, 203 | ... 204 | )) 205 | ) 206 | } 207 | -------------------------------------------------------------------------------- /R/commands.R: -------------------------------------------------------------------------------- 1 | req_command <- function(req, command, session_id = NULL, element_id = NULL, shadow_id = NULL, ...) { 2 | stopifnot(command %in% names(commands)) 3 | command_list <- commands[[command]] 4 | method <- command_list$method 5 | url <- command_list$url 6 | if (!is.null(session_id)) { 7 | url <- gsub("{session id}", session_id, url, fixed = TRUE) 8 | } 9 | if (!is.null(element_id)) { 10 | url <- gsub("{element id}", element_id, url, fixed = TRUE) 11 | } 12 | if (!is.null(shadow_id)) { 13 | url <- gsub("{shadow id}", shadow_id, url, fixed = TRUE) 14 | } 15 | 16 | extra_elements <- rlang::list2(...) 17 | for (a in names(extra_elements)) { 18 | url <- gsub(paste0("{", a, "}"), extra_elements[[a]], url, fixed = TRUE) 19 | } 20 | 21 | httr2::req_method(httr2::req_url_path_append(req, url), method) 22 | } 23 | 24 | req_body_selenium <- function(req, body, request_body = NULL) { 25 | if (!is.null(request_body)) { 26 | body <- request_body 27 | } 28 | 29 | body <- jsonlite::toJSON(body, auto_unbox = TRUE) 30 | req <- httr2::req_body_raw(req, body) 31 | req <- httr2::req_headers( 32 | req, 33 | "Content-Type" = "application/json; charset=utf-8", 34 | "Accept" = "application/json; charset=utf-8" 35 | ) 36 | req 37 | } 38 | 39 | req_perform_selenium <- function(req, verbose = FALSE, timeout = NULL, call = rlang::caller_env()) { 40 | if (verbose) { 41 | req <- httr2::req_verbose(req) 42 | } 43 | 44 | req <- httr2::req_error(req, body = extract_error_message) 45 | 46 | if (!is.null(timeout)) { 47 | req <- httr2::req_timeout(req, timeout) 48 | } 49 | 50 | rlang::local_error_call(call) 51 | 52 | httr2::req_perform(req) 53 | } 54 | 55 | extract_error_message <- function(resp) { 56 | if (httr2::resp_content_type(resp) != "application/json") { 57 | return(NULL) 58 | } 59 | 60 | body <- httr2::resp_body_json(resp) 61 | 62 | if (is.list(body$value)) { 63 | error <- body$value$error 64 | message <- body$value$message 65 | } else { 66 | error <- body$error 67 | message <- body$message 68 | } 69 | message <- gsub(paste0(error, ": "), "", message, fixed = TRUE) 70 | 71 | c( 72 | "x" = paste0(to_sentence_case(error), "."), 73 | "x" = indent_message(message) 74 | ) 75 | } 76 | 77 | indent_message <- function(x) { 78 | lines <- strsplit(x, "\n", fixed = TRUE)[[1]] 79 | lines[-1] <- paste0(" ", lines[-1]) 80 | paste(lines, collapse = "\n") 81 | } 82 | -------------------------------------------------------------------------------- /R/import-standalone-obj-type.R: -------------------------------------------------------------------------------- 1 | # Standalone file: do not edit by hand 2 | # Source: 3 | # ---------------------------------------------------------------------- 4 | # 5 | # --- 6 | # repo: r-lib/rlang 7 | # file: standalone-obj-type.R 8 | # last-updated: 2023-05-01 9 | # license: https://unlicense.org 10 | # imports: rlang (>= 1.1.0) 11 | # --- 12 | # 13 | # ## Changelog 14 | # 15 | # 2023-05-01: 16 | # - `obj_type_friendly()` now only displays the first class of S3 objects. 17 | # 18 | # 2023-03-30: 19 | # - `stop_input_type()` now handles `I()` input literally in `arg`. 20 | # 21 | # 2022-10-04: 22 | # - `obj_type_friendly(value = TRUE)` now shows numeric scalars 23 | # literally. 24 | # - `stop_friendly_type()` now takes `show_value`, passed to 25 | # `obj_type_friendly()` as the `value` argument. 26 | # 27 | # 2022-10-03: 28 | # - Added `allow_na` and `allow_null` arguments. 29 | # - `NULL` is now backticked. 30 | # - Better friendly type for infinities and `NaN`. 31 | # 32 | # 2022-09-16: 33 | # - Unprefixed usage of rlang functions with `rlang::` to 34 | # avoid onLoad issues when called from rlang (#1482). 35 | # 36 | # 2022-08-11: 37 | # - Prefixed usage of rlang functions with `rlang::`. 38 | # 39 | # 2022-06-22: 40 | # - `friendly_type_of()` is now `obj_type_friendly()`. 41 | # - Added `obj_type_oo()`. 42 | # 43 | # 2021-12-20: 44 | # - Added support for scalar values and empty vectors. 45 | # - Added `stop_input_type()` 46 | # 47 | # 2021-06-30: 48 | # - Added support for missing arguments. 49 | # 50 | # 2021-04-19: 51 | # - Added support for matrices and arrays (#141). 52 | # - Added documentation. 53 | # - Added changelog. 54 | # 55 | # nocov start 56 | 57 | #' Return English-friendly type 58 | #' @param x Any R object. 59 | #' @param value Whether to describe the value of `x`. Special values 60 | #' like `NA` or `""` are always described. 61 | #' @param length Whether to mention the length of vectors and lists. 62 | #' @return A string describing the type. Starts with an indefinite 63 | #' article, e.g. "an integer vector". 64 | #' @noRd 65 | obj_type_friendly <- function(x, value = TRUE) { 66 | if (is_missing(x)) { 67 | return("absent") 68 | } 69 | 70 | if (is.object(x)) { 71 | if (inherits(x, "quosure")) { 72 | type <- "quosure" 73 | } else { 74 | type <- class(x)[[1L]] 75 | } 76 | return(sprintf("a <%s> object", type)) 77 | } 78 | 79 | if (!is_vector(x)) { 80 | return(.rlang_as_friendly_type(typeof(x))) 81 | } 82 | 83 | n_dim <- length(dim(x)) 84 | 85 | if (!n_dim) { 86 | if (!is_list(x) && length(x) == 1) { 87 | if (is_na(x)) { 88 | return(switch( 89 | typeof(x), 90 | logical = "`NA`", 91 | integer = "an integer `NA`", 92 | double = 93 | if (is.nan(x)) { 94 | "`NaN`" 95 | } else { 96 | "a numeric `NA`" 97 | }, 98 | complex = "a complex `NA`", 99 | character = "a character `NA`", 100 | .rlang_stop_unexpected_typeof(x) 101 | )) 102 | } 103 | 104 | show_infinites <- function(x) { 105 | if (x > 0) { 106 | "`Inf`" 107 | } else { 108 | "`-Inf`" 109 | } 110 | } 111 | str_encode <- function(x, width = 30, ...) { 112 | if (nchar(x) > width) { 113 | x <- substr(x, 1, width - 3) 114 | x <- paste0(x, "...") 115 | } 116 | encodeString(x, ...) 117 | } 118 | 119 | if (value) { 120 | if (is.numeric(x) && is.infinite(x)) { 121 | return(show_infinites(x)) 122 | } 123 | 124 | if (is.numeric(x) || is.complex(x)) { 125 | number <- as.character(round(x, 2)) 126 | what <- if (is.complex(x)) "the complex number" else "the number" 127 | return(paste(what, number)) 128 | } 129 | 130 | return(switch( 131 | typeof(x), 132 | logical = if (x) "`TRUE`" else "`FALSE`", 133 | character = { 134 | what <- if (nzchar(x)) "the string" else "the empty string" 135 | paste(what, str_encode(x, quote = "\"")) 136 | }, 137 | raw = paste("the raw value", as.character(x)), 138 | .rlang_stop_unexpected_typeof(x) 139 | )) 140 | } 141 | 142 | return(switch( 143 | typeof(x), 144 | logical = "a logical value", 145 | integer = "an integer", 146 | double = if (is.infinite(x)) show_infinites(x) else "a number", 147 | complex = "a complex number", 148 | character = if (nzchar(x)) "a string" else "\"\"", 149 | raw = "a raw value", 150 | .rlang_stop_unexpected_typeof(x) 151 | )) 152 | } 153 | 154 | if (length(x) == 0) { 155 | return(switch( 156 | typeof(x), 157 | logical = "an empty logical vector", 158 | integer = "an empty integer vector", 159 | double = "an empty numeric vector", 160 | complex = "an empty complex vector", 161 | character = "an empty character vector", 162 | raw = "an empty raw vector", 163 | list = "an empty list", 164 | .rlang_stop_unexpected_typeof(x) 165 | )) 166 | } 167 | } 168 | 169 | vec_type_friendly(x) 170 | } 171 | 172 | vec_type_friendly <- function(x, length = FALSE) { 173 | if (!is_vector(x)) { 174 | abort("`x` must be a vector.") 175 | } 176 | type <- typeof(x) 177 | n_dim <- length(dim(x)) 178 | 179 | add_length <- function(type) { 180 | if (length && !n_dim) { 181 | paste0(type, sprintf(" of length %s", length(x))) 182 | } else { 183 | type 184 | } 185 | } 186 | 187 | if (type == "list") { 188 | if (n_dim < 2) { 189 | return(add_length("a list")) 190 | } else if (is.data.frame(x)) { 191 | return("a data frame") 192 | } else if (n_dim == 2) { 193 | return("a list matrix") 194 | } else { 195 | return("a list array") 196 | } 197 | } 198 | 199 | type <- switch( 200 | type, 201 | logical = "a logical %s", 202 | integer = "an integer %s", 203 | numeric = , 204 | double = "a double %s", 205 | complex = "a complex %s", 206 | character = "a character %s", 207 | raw = "a raw %s", 208 | type = paste0("a ", type, " %s") 209 | ) 210 | 211 | if (n_dim < 2) { 212 | kind <- "vector" 213 | } else if (n_dim == 2) { 214 | kind <- "matrix" 215 | } else { 216 | kind <- "array" 217 | } 218 | out <- sprintf(type, kind) 219 | 220 | if (n_dim >= 2) { 221 | out 222 | } else { 223 | add_length(out) 224 | } 225 | } 226 | 227 | .rlang_as_friendly_type <- function(type) { 228 | switch( 229 | type, 230 | 231 | list = "a list", 232 | 233 | NULL = "`NULL`", 234 | environment = "an environment", 235 | externalptr = "a pointer", 236 | weakref = "a weak reference", 237 | S4 = "an S4 object", 238 | 239 | name = , 240 | symbol = "a symbol", 241 | language = "a call", 242 | pairlist = "a pairlist node", 243 | expression = "an expression vector", 244 | 245 | char = "an internal string", 246 | promise = "an internal promise", 247 | ... = "an internal dots object", 248 | any = "an internal `any` object", 249 | bytecode = "an internal bytecode object", 250 | 251 | primitive = , 252 | builtin = , 253 | special = "a primitive function", 254 | closure = "a function", 255 | 256 | type 257 | ) 258 | } 259 | 260 | .rlang_stop_unexpected_typeof <- function(x, call = caller_env()) { 261 | abort( 262 | sprintf("Unexpected type <%s>.", typeof(x)), 263 | call = call 264 | ) 265 | } 266 | 267 | #' Return OO type 268 | #' @param x Any R object. 269 | #' @return One of `"bare"` (for non-OO objects), `"S3"`, `"S4"`, 270 | #' `"R6"`, or `"R7"`. 271 | #' @noRd 272 | obj_type_oo <- function(x) { 273 | if (!is.object(x)) { 274 | return("bare") 275 | } 276 | 277 | class <- inherits(x, c("R6", "R7_object"), which = TRUE) 278 | 279 | if (class[[1]]) { 280 | "R6" 281 | } else if (class[[2]]) { 282 | "R7" 283 | } else if (isS4(x)) { 284 | "S4" 285 | } else { 286 | "S3" 287 | } 288 | } 289 | 290 | #' @param x The object type which does not conform to `what`. Its 291 | #' `obj_type_friendly()` is taken and mentioned in the error message. 292 | #' @param what The friendly expected type as a string. Can be a 293 | #' character vector of expected types, in which case the error 294 | #' message mentions all of them in an "or" enumeration. 295 | #' @param show_value Passed to `value` argument of `obj_type_friendly()`. 296 | #' @param ... Arguments passed to [abort()]. 297 | #' @inheritParams args_error_context 298 | #' @noRd 299 | stop_input_type <- function(x, 300 | what, 301 | ..., 302 | allow_na = FALSE, 303 | allow_null = FALSE, 304 | show_value = TRUE, 305 | arg = caller_arg(x), 306 | call = caller_env()) { 307 | # From standalone-cli.R 308 | cli <- env_get_list( 309 | nms = c("format_arg", "format_code"), 310 | last = topenv(), 311 | default = function(x) sprintf("`%s`", x), 312 | inherit = TRUE 313 | ) 314 | 315 | if (allow_na) { 316 | what <- c(what, cli$format_code("NA")) 317 | } 318 | if (allow_null) { 319 | what <- c(what, cli$format_code("NULL")) 320 | } 321 | if (length(what)) { 322 | what <- oxford_comma(what) 323 | } 324 | if (inherits(arg, "AsIs")) { 325 | format_arg <- identity 326 | } else { 327 | format_arg <- cli$format_arg 328 | } 329 | 330 | message <- sprintf( 331 | "%s must be %s, not %s.", 332 | format_arg(arg), 333 | what, 334 | obj_type_friendly(x, value = show_value) 335 | ) 336 | 337 | abort(message, ..., call = call, arg = arg) 338 | } 339 | 340 | oxford_comma <- function(chr, sep = ", ", final = "or") { 341 | n <- length(chr) 342 | 343 | if (n < 2) { 344 | return(chr) 345 | } 346 | 347 | head <- chr[seq_len(n - 1)] 348 | last <- chr[n] 349 | 350 | head <- paste(head, collapse = sep) 351 | 352 | # Write a or b. But a, b, or c. 353 | if (n > 2) { 354 | paste0(head, sep, final, " ", last) 355 | } else { 356 | paste0(head, " ", final, " ", last) 357 | } 358 | } 359 | 360 | # nocov end 361 | -------------------------------------------------------------------------------- /R/import-standalone-types-check.R: -------------------------------------------------------------------------------- 1 | # Standalone file: do not edit by hand 2 | # Source: 3 | # ---------------------------------------------------------------------- 4 | # 5 | # --- 6 | # repo: r-lib/rlang 7 | # file: standalone-types-check.R 8 | # last-updated: 2023-03-13 9 | # license: https://unlicense.org 10 | # dependencies: standalone-obj-type.R 11 | # imports: rlang (>= 1.1.0) 12 | # --- 13 | # 14 | # ## Changelog 15 | # 16 | # 2023-03-13: 17 | # - Improved error messages of number checkers (@teunbrand) 18 | # - Added `allow_infinite` argument to `check_number_whole()` (@mgirlich). 19 | # - Added `check_data_frame()` (@mgirlich). 20 | # 21 | # 2023-03-07: 22 | # - Added dependency on rlang (>= 1.1.0). 23 | # 24 | # 2023-02-15: 25 | # - Added `check_logical()`. 26 | # 27 | # - `check_bool()`, `check_number_whole()`, and 28 | # `check_number_decimal()` are now implemented in C. 29 | # 30 | # - For efficiency, `check_number_whole()` and 31 | # `check_number_decimal()` now take a `NULL` default for `min` and 32 | # `max`. This makes it possible to bypass unnecessary type-checking 33 | # and comparisons in the default case of no bounds checks. 34 | # 35 | # 2022-10-07: 36 | # - `check_number_whole()` and `_decimal()` no longer treat 37 | # non-numeric types such as factors or dates as numbers. Numeric 38 | # types are detected with `is.numeric()`. 39 | # 40 | # 2022-10-04: 41 | # - Added `check_name()` that forbids the empty string. 42 | # `check_string()` allows the empty string by default. 43 | # 44 | # 2022-09-28: 45 | # - Removed `what` arguments. 46 | # - Added `allow_na` and `allow_null` arguments. 47 | # - Added `allow_decimal` and `allow_infinite` arguments. 48 | # - Improved errors with absent arguments. 49 | # 50 | # 51 | # 2022-09-16: 52 | # - Unprefixed usage of rlang functions with `rlang::` to 53 | # avoid onLoad issues when called from rlang (#1482). 54 | # 55 | # 2022-08-11: 56 | # - Added changelog. 57 | # 58 | # nocov start 59 | 60 | # Scalars ----------------------------------------------------------------- 61 | 62 | .standalone_types_check_dot_call <- .Call 63 | 64 | check_bool <- function(x, 65 | ..., 66 | allow_na = FALSE, 67 | allow_null = FALSE, 68 | arg = caller_arg(x), 69 | call = caller_env()) { 70 | if (!missing(x) && .standalone_types_check_dot_call(ffi_standalone_is_bool_1.0.7, x, allow_na, allow_null)) { 71 | return(invisible(NULL)) 72 | } 73 | 74 | stop_input_type( 75 | x, 76 | c("`TRUE`", "`FALSE`"), 77 | ..., 78 | allow_na = allow_na, 79 | allow_null = allow_null, 80 | arg = arg, 81 | call = call 82 | ) 83 | } 84 | 85 | check_string <- function(x, 86 | ..., 87 | allow_empty = TRUE, 88 | allow_na = FALSE, 89 | allow_null = FALSE, 90 | arg = caller_arg(x), 91 | call = caller_env()) { 92 | if (!missing(x)) { 93 | is_string <- .rlang_check_is_string( 94 | x, 95 | allow_empty = allow_empty, 96 | allow_na = allow_na, 97 | allow_null = allow_null 98 | ) 99 | if (is_string) { 100 | return(invisible(NULL)) 101 | } 102 | } 103 | 104 | stop_input_type( 105 | x, 106 | "a single string", 107 | ..., 108 | allow_na = allow_na, 109 | allow_null = allow_null, 110 | arg = arg, 111 | call = call 112 | ) 113 | } 114 | 115 | .rlang_check_is_string <- function(x, 116 | allow_empty, 117 | allow_na, 118 | allow_null) { 119 | if (is_string(x)) { 120 | if (allow_empty || !is_string(x, "")) { 121 | return(TRUE) 122 | } 123 | } 124 | 125 | if (allow_null && is_null(x)) { 126 | return(TRUE) 127 | } 128 | 129 | if (allow_na && (identical(x, NA) || identical(x, na_chr))) { 130 | return(TRUE) 131 | } 132 | 133 | FALSE 134 | } 135 | 136 | check_name <- function(x, 137 | ..., 138 | allow_null = FALSE, 139 | arg = caller_arg(x), 140 | call = caller_env()) { 141 | if (!missing(x)) { 142 | is_string <- .rlang_check_is_string( 143 | x, 144 | allow_empty = FALSE, 145 | allow_na = FALSE, 146 | allow_null = allow_null 147 | ) 148 | if (is_string) { 149 | return(invisible(NULL)) 150 | } 151 | } 152 | 153 | stop_input_type( 154 | x, 155 | "a valid name", 156 | ..., 157 | allow_na = FALSE, 158 | allow_null = allow_null, 159 | arg = arg, 160 | call = call 161 | ) 162 | } 163 | 164 | IS_NUMBER_true <- 0 165 | IS_NUMBER_false <- 1 166 | IS_NUMBER_oob <- 2 167 | 168 | check_number_decimal <- function(x, 169 | ..., 170 | min = NULL, 171 | max = NULL, 172 | allow_infinite = TRUE, 173 | allow_na = FALSE, 174 | allow_null = FALSE, 175 | arg = caller_arg(x), 176 | call = caller_env()) { 177 | if (missing(x)) { 178 | exit_code <- IS_NUMBER_false 179 | } else if (0 == (exit_code <- .standalone_types_check_dot_call( 180 | ffi_standalone_check_number_1.0.7, 181 | x, 182 | allow_decimal = TRUE, 183 | min, 184 | max, 185 | allow_infinite, 186 | allow_na, 187 | allow_null 188 | ))) { 189 | return(invisible(NULL)) 190 | } 191 | 192 | .stop_not_number( 193 | x, 194 | ..., 195 | exit_code = exit_code, 196 | allow_decimal = TRUE, 197 | min = min, 198 | max = max, 199 | allow_na = allow_na, 200 | allow_null = allow_null, 201 | arg = arg, 202 | call = call 203 | ) 204 | } 205 | 206 | check_number_whole <- function(x, 207 | ..., 208 | min = NULL, 209 | max = NULL, 210 | allow_infinite = FALSE, 211 | allow_na = FALSE, 212 | allow_null = FALSE, 213 | arg = caller_arg(x), 214 | call = caller_env()) { 215 | if (missing(x)) { 216 | exit_code <- IS_NUMBER_false 217 | } else if (0 == (exit_code <- .standalone_types_check_dot_call( 218 | ffi_standalone_check_number_1.0.7, 219 | x, 220 | allow_decimal = FALSE, 221 | min, 222 | max, 223 | allow_infinite, 224 | allow_na, 225 | allow_null 226 | ))) { 227 | return(invisible(NULL)) 228 | } 229 | 230 | .stop_not_number( 231 | x, 232 | ..., 233 | exit_code = exit_code, 234 | allow_decimal = FALSE, 235 | min = min, 236 | max = max, 237 | allow_na = allow_na, 238 | allow_null = allow_null, 239 | arg = arg, 240 | call = call 241 | ) 242 | } 243 | 244 | .stop_not_number <- function(x, 245 | ..., 246 | exit_code, 247 | allow_decimal, 248 | min, 249 | max, 250 | allow_na, 251 | allow_null, 252 | arg, 253 | call) { 254 | if (allow_decimal) { 255 | what <- "a number" 256 | } else { 257 | what <- "a whole number" 258 | } 259 | 260 | if (exit_code == IS_NUMBER_oob) { 261 | min <- min %||% -Inf 262 | max <- max %||% Inf 263 | 264 | if (min > -Inf && max < Inf) { 265 | what <- sprintf("%s between %s and %s", what, min, max) 266 | } else if (x < min) { 267 | what <- sprintf("%s larger than or equal to %s", what, min) 268 | } else if (x > max) { 269 | what <- sprintf("%s smaller than or equal to %s", what, max) 270 | } else { 271 | abort("Unexpected state in OOB check", .internal = TRUE) 272 | } 273 | } 274 | 275 | stop_input_type( 276 | x, 277 | what, 278 | ..., 279 | allow_na = allow_na, 280 | allow_null = allow_null, 281 | arg = arg, 282 | call = call 283 | ) 284 | } 285 | 286 | check_symbol <- function(x, 287 | ..., 288 | allow_null = FALSE, 289 | arg = caller_arg(x), 290 | call = caller_env()) { 291 | if (!missing(x)) { 292 | if (is_symbol(x)) { 293 | return(invisible(NULL)) 294 | } 295 | if (allow_null && is_null(x)) { 296 | return(invisible(NULL)) 297 | } 298 | } 299 | 300 | stop_input_type( 301 | x, 302 | "a symbol", 303 | ..., 304 | allow_na = FALSE, 305 | allow_null = allow_null, 306 | arg = arg, 307 | call = call 308 | ) 309 | } 310 | 311 | check_arg <- function(x, 312 | ..., 313 | allow_null = FALSE, 314 | arg = caller_arg(x), 315 | call = caller_env()) { 316 | if (!missing(x)) { 317 | if (is_symbol(x)) { 318 | return(invisible(NULL)) 319 | } 320 | if (allow_null && is_null(x)) { 321 | return(invisible(NULL)) 322 | } 323 | } 324 | 325 | stop_input_type( 326 | x, 327 | "an argument name", 328 | ..., 329 | allow_na = FALSE, 330 | allow_null = allow_null, 331 | arg = arg, 332 | call = call 333 | ) 334 | } 335 | 336 | check_call <- function(x, 337 | ..., 338 | allow_null = FALSE, 339 | arg = caller_arg(x), 340 | call = caller_env()) { 341 | if (!missing(x)) { 342 | if (is_call(x)) { 343 | return(invisible(NULL)) 344 | } 345 | if (allow_null && is_null(x)) { 346 | return(invisible(NULL)) 347 | } 348 | } 349 | 350 | stop_input_type( 351 | x, 352 | "a defused call", 353 | ..., 354 | allow_na = FALSE, 355 | allow_null = allow_null, 356 | arg = arg, 357 | call = call 358 | ) 359 | } 360 | 361 | check_environment <- function(x, 362 | ..., 363 | allow_null = FALSE, 364 | arg = caller_arg(x), 365 | call = caller_env()) { 366 | if (!missing(x)) { 367 | if (is_environment(x)) { 368 | return(invisible(NULL)) 369 | } 370 | if (allow_null && is_null(x)) { 371 | return(invisible(NULL)) 372 | } 373 | } 374 | 375 | stop_input_type( 376 | x, 377 | "an environment", 378 | ..., 379 | allow_na = FALSE, 380 | allow_null = allow_null, 381 | arg = arg, 382 | call = call 383 | ) 384 | } 385 | 386 | check_function <- function(x, 387 | ..., 388 | allow_null = FALSE, 389 | arg = caller_arg(x), 390 | call = caller_env()) { 391 | if (!missing(x)) { 392 | if (is_function(x)) { 393 | return(invisible(NULL)) 394 | } 395 | if (allow_null && is_null(x)) { 396 | return(invisible(NULL)) 397 | } 398 | } 399 | 400 | stop_input_type( 401 | x, 402 | "a function", 403 | ..., 404 | allow_na = FALSE, 405 | allow_null = allow_null, 406 | arg = arg, 407 | call = call 408 | ) 409 | } 410 | 411 | check_closure <- function(x, 412 | ..., 413 | allow_null = FALSE, 414 | arg = caller_arg(x), 415 | call = caller_env()) { 416 | if (!missing(x)) { 417 | if (is_closure(x)) { 418 | return(invisible(NULL)) 419 | } 420 | if (allow_null && is_null(x)) { 421 | return(invisible(NULL)) 422 | } 423 | } 424 | 425 | stop_input_type( 426 | x, 427 | "an R function", 428 | ..., 429 | allow_na = FALSE, 430 | allow_null = allow_null, 431 | arg = arg, 432 | call = call 433 | ) 434 | } 435 | 436 | check_formula <- function(x, 437 | ..., 438 | allow_null = FALSE, 439 | arg = caller_arg(x), 440 | call = caller_env()) { 441 | if (!missing(x)) { 442 | if (is_formula(x)) { 443 | return(invisible(NULL)) 444 | } 445 | if (allow_null && is_null(x)) { 446 | return(invisible(NULL)) 447 | } 448 | } 449 | 450 | stop_input_type( 451 | x, 452 | "a formula", 453 | ..., 454 | allow_na = FALSE, 455 | allow_null = allow_null, 456 | arg = arg, 457 | call = call 458 | ) 459 | } 460 | 461 | 462 | # Vectors ----------------------------------------------------------------- 463 | 464 | check_character <- function(x, 465 | ..., 466 | allow_null = FALSE, 467 | arg = caller_arg(x), 468 | call = caller_env()) { 469 | if (!missing(x)) { 470 | if (is_character(x)) { 471 | return(invisible(NULL)) 472 | } 473 | if (allow_null && is_null(x)) { 474 | return(invisible(NULL)) 475 | } 476 | } 477 | 478 | stop_input_type( 479 | x, 480 | "a character vector", 481 | ..., 482 | allow_na = FALSE, 483 | allow_null = allow_null, 484 | arg = arg, 485 | call = call 486 | ) 487 | } 488 | 489 | check_logical <- function(x, 490 | ..., 491 | allow_null = FALSE, 492 | arg = caller_arg(x), 493 | call = caller_env()) { 494 | if (!missing(x)) { 495 | if (is_logical(x)) { 496 | return(invisible(NULL)) 497 | } 498 | if (allow_null && is_null(x)) { 499 | return(invisible(NULL)) 500 | } 501 | } 502 | 503 | stop_input_type( 504 | x, 505 | "a logical vector", 506 | ..., 507 | allow_na = FALSE, 508 | allow_null = allow_null, 509 | arg = arg, 510 | call = call 511 | ) 512 | } 513 | 514 | check_data_frame <- function(x, 515 | ..., 516 | allow_null = FALSE, 517 | arg = caller_arg(x), 518 | call = caller_env()) { 519 | if (!missing(x)) { 520 | if (is.data.frame(x)) { 521 | return(invisible(NULL)) 522 | } 523 | if (allow_null && is_null(x)) { 524 | return(invisible(NULL)) 525 | } 526 | } 527 | 528 | stop_input_type( 529 | x, 530 | "a data frame", 531 | ..., 532 | allow_null = allow_null, 533 | arg = arg, 534 | call = call 535 | ) 536 | } 537 | 538 | # nocov end 539 | -------------------------------------------------------------------------------- /R/json.R: -------------------------------------------------------------------------------- 1 | prepare_for_json <- function(x) { 2 | if (inherits_any(x, c("WebElement", "ShadowRoot"))) { 3 | x$toJSON() 4 | } else if (is_bare_list(x)) { 5 | lapply(x, prepare_for_json) 6 | } else { 7 | x 8 | } 9 | } 10 | 11 | parse_json_result <- function(x, self) { 12 | if (rlang::is_bare_list(x) && length(x) == 1 && names(x) %in% c(web_element_id, shadow_element_id)) { 13 | if (names(x) == web_element_id) { 14 | self$create_webelement(x[[1]]) 15 | } else { 16 | self$create_shadowroot(x[[1]]) 17 | } 18 | } else if (rlang::is_bare_list(x)) { 19 | lapply(x, parse_json_result, self) 20 | } else { 21 | x 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /R/keys.R: -------------------------------------------------------------------------------- 1 | # https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/Keys.java 2 | #' A list of special keys 3 | #' 4 | #' A named list of special keys, where each key is a single Unicode character, 5 | #' which will be interpreted by selenium as a special key. Each key is just a 6 | #' string, so can be used with string manipulation functions like [paste()] 7 | #' without any special treatment. 8 | #' 9 | #' @examples 10 | #' keys$enter 11 | #' 12 | #' @export 13 | keys <- list( 14 | null = "\ue000", 15 | cancel = "\ue001", # ^break 16 | help = "\ue002", 17 | backspace = "\ue003", 18 | back_space = "\ue003", 19 | tab = "\ue004", 20 | clear = "\ue005", 21 | return = "\ue006", 22 | enter = "\ue007", 23 | shift = "\ue008", 24 | left_shift = "\ue008", 25 | control = "\ue009", 26 | left_control = "\ue009", 27 | alt = "\ue00a", 28 | left_alt = "\ue00a", 29 | pause = "\ue00b", 30 | escape = "\ue00c", 31 | space = "\ue00d", 32 | page_up = "\ue00e", 33 | page_down = "\ue00f", 34 | end = "\ue010", 35 | home = "\ue011", 36 | left = "\ue012", 37 | arrow_left = "\ue012", 38 | up = "\ue013", 39 | arrow_up = "\ue013", 40 | right = "\ue014", 41 | arrow_right = "\ue014", 42 | down = "\ue015", 43 | arrow_down = "\ue015", 44 | insert = "\ue016", 45 | delete = "\ue017", 46 | semicolon = "\ue018", 47 | equals = "\ue019", 48 | numpad0 = "\ue01a", # number pad keys 49 | numpad1 = "\ue01b", 50 | numpad2 = "\ue01c", 51 | numpad3 = "\ue01d", 52 | numpad4 = "\ue01e", 53 | numpad5 = "\ue01f", 54 | numpad6 = "\ue020", 55 | numpad7 = "\ue021", 56 | numpad8 = "\ue022", 57 | numpad9 = "\ue023", 58 | multiply = "\ue024", 59 | add = "\ue025", 60 | separator = "\ue026", 61 | subtract = "\ue027", 62 | decimal = "\ue028", 63 | divide = "\ue029", 64 | f1 = "\ue031", # function keys 65 | f2 = "\ue032", 66 | f3 = "\ue033", 67 | f4 = "\ue034", 68 | f5 = "\ue035", 69 | f6 = "\ue036", 70 | f7 = "\ue037", 71 | f8 = "\ue038", 72 | f9 = "\ue039", 73 | f10 = "\ue03a", 74 | f11 = "\ue03b", 75 | f12 = "\ue03c", 76 | meta = "\ue03d", 77 | command = "\ue03d", 78 | zenkaku_hankaku = "\ue040" 79 | ) 80 | 81 | #' Combine special keys 82 | #' 83 | #' When a chord of keys is passed into `WebElement$send_keys()`, all keys will 84 | #' be pressed in order, and then released at the end. This is simply done by 85 | #' combining the keys into a single string, and appending the NULL key 86 | #' ([keys$null][keys]) to the end. This is useful for keybindings like 87 | #' `Ctrl-V`, where you want the Ctrl key to be released after the action. 88 | #' 89 | #' @param ... The keys to be combined (strings). 90 | #' 91 | #' @returns A string. 92 | #' 93 | #' @examples 94 | #' # `Ctrl-V` will be pressed, then `Ctrl-Alt-V` 95 | #' paste0( 96 | #' keys$control, "v", 97 | #' keys$alt, "v" 98 | #' ) 99 | #' 100 | #' # `Ctrl-V` will be pressed, then `Alt-V` 101 | #' paste0( 102 | #' key_chord(keys$control, "v"), 103 | #' key_chord(keys$alt, "v") 104 | #' ) 105 | #' 106 | #' @export 107 | key_chord <- function(...) { 108 | rlang::check_dots_unnamed() 109 | 110 | paste0(..., keys$null) 111 | } 112 | -------------------------------------------------------------------------------- /R/selenium-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | ## usethis namespace: start 5 | #' @importFrom lifecycle deprecated 6 | #' @importFrom R6 is.R6Class 7 | #' @importFrom processx process 8 | #' @import rlang 9 | ## usethis namespace: end 10 | NULL 11 | -------------------------------------------------------------------------------- /R/server.R: -------------------------------------------------------------------------------- 1 | # Store the latest version name in an environment, that will persist until 2 | # the R session is closed. 3 | selenium <- rlang::new_environment() 4 | 5 | get_selenium_env <- function() { 6 | utils::getFromNamespace("selenium", ns = "selenium") 7 | } 8 | 9 | get_from_env <- function(items) { 10 | sel_env <- get_selenium_env() 11 | env_get(sel_env, items, default = NULL) 12 | } 13 | 14 | set_in_env <- function(...) { 15 | sel_env <- get_selenium_env() 16 | env_bind(sel_env, ...) 17 | } 18 | 19 | #' Download and start the Selenium server. 20 | #' 21 | #' @description 22 | #' `r lifecycle::badge('experimental')` 23 | #' 24 | #' Downloads the latest release of Selenium Server, and then runs it as a 25 | #' background process. You must have Java installed for this command to work. 26 | #' 27 | #' @param version The version of Selenium Server to download and run. By 28 | #' default, the latest major or minor release is used. 29 | #' @param host The host to run the server on. 30 | #' @param port The port to run the server on. 31 | #' @param selenium_manager Whether to enable Selenium Manager, which will 32 | #' automatically download any missing drivers. Defaults to `TRUE`. 33 | #' @param interactive By default, if you don't have a version downloaded, you 34 | #' will be prompted to confirm that you want to download it, and the function 35 | #' will error if [rlang::is_interactive()] returns `FALSE`. To allow this 36 | #' function to work in a non-interactive setting, set this to `FALSE`. 37 | #' @param stdout,stderr Passed into 38 | #' [processx::process$new()][processx::process]. Set to `"|"` to capture 39 | #' output and error logs from the server, which can then be accessed with 40 | #' `server$read_output()` and `server$read_error()`. 41 | #' @param verbose Passed into [utils::download.file()]. Note that setting this 42 | #' to `FALSE` will *not* disable the prompt if a file needs to be downloaded. 43 | #' @param temp Whether to use a temporary directory to download the Selenium 44 | #' Server `.jar` file. This will ensure that the file is deleted after it is 45 | #' used, but means that you will have to redownload the file with every new 46 | #' R session. If `FALSE`, the file is saved in your user data directory. 47 | #' @param path The path where the downloaded Selenium Server `.jar` file will 48 | #' be saved. Overrides `temp`. 49 | #' @param extra_args A character vector of extra arguments to pass into the 50 | #' Selenium Server call. See the list of options here: 51 | #' 52 | #' @param ... Passed into [processx::process$new()][processx::process]. 53 | #' 54 | #' @returns A [SeleniumServer] object. Call `server$kill()` to stop the 55 | #' server. 56 | #' 57 | #' @details 58 | #' This command respects the `JAVA_HOME` environment variable when attempting 59 | #' to find the `java` executable. Otherwise, [Sys.which()] is used. 60 | #' 61 | #' @seealso 62 | #' The [package website](https://ashbythorpe.github.io/selenium-r/index.html) 63 | #' for more ways to start the Selenium server. 64 | #' 65 | #' @examples 66 | #' \dontrun{ 67 | #' # Disables the prompt that asks you whether you want to download Selenium server 68 | #' server <- selenium_server(interactive = FALSE) 69 | #' 70 | #' # Saves the server in your user data directory 71 | #' server <- selenium_server(temp = FALSE) 72 | #' server$kill() 73 | #' 74 | #' # The server doesn't have to be downloaded again 75 | #' server <- selenium_server(temp = FALSE) 76 | #' 77 | #' # Here we use extra arguments to increase the timeout of client sessions, 78 | #' # allowing sessions to stay open for longer without being automatically 79 | #' # terminated. 80 | #' server <- selenium_server(extra_args = c("--session-timeout", "3000")) 81 | #' } 82 | #' 83 | #' @export 84 | selenium_server <- function(version = "latest", 85 | host = "localhost", 86 | port = 4444L, 87 | selenium_manager = TRUE, 88 | interactive = TRUE, 89 | stdout = NULL, 90 | stderr = NULL, 91 | verbose = TRUE, 92 | temp = TRUE, 93 | path = NULL, 94 | extra_args = c(), 95 | ...) { 96 | check_string(version) 97 | check_string(host) 98 | check_number_whole(port) 99 | check_bool(selenium_manager) 100 | check_bool(interactive) 101 | check_bool(verbose) 102 | check_bool(temp) 103 | check_string(path, allow_null = TRUE) 104 | check_character(extra_args, allow_null = TRUE) 105 | 106 | if (version != "latest") { 107 | n_version <- numeric_version(version) 108 | 109 | list_version <- unclass(n_version) 110 | if (list_version[[1]][length(list_version[[1]])] != 0) { 111 | list_version[[1]][length(list_version[[1]])] <- 0 112 | class(list_version) <- class(n_version) 113 | 114 | rlang::abort(c( 115 | "`version` must be a major or minor release, not a patch.", 116 | "i" = paste0("Supplied version: ", version), 117 | "i" = paste0("Did you mean: ", list_version, "?") 118 | )) 119 | } 120 | 121 | if (n_version < "4.9.0" && selenium_manager) { 122 | rlang::warn(c( 123 | "Selenium Server 4.9.0 or higher is required to use Selenium Manager.", 124 | "x" = paste0("Actual version requested: ", n_version, "."), 125 | "x" = "Disabling Selenium Manager.", 126 | "i" = "Set `selenium_manager` to `FALSE` to disable this warning." 127 | )) 128 | selenium_manager <- FALSE 129 | } 130 | } 131 | 132 | if (version == "latest") { 133 | release_name <- rlang::try_fetch(get_latest_version_name(), error = identity) 134 | if (rlang::is_error(release_name)) { 135 | version <- get_version_from_files(release_name) 136 | release_name <- paste0("selenium-", version) 137 | } else { 138 | version <- gsub("^selenium-", "", release_name) 139 | } 140 | } else { 141 | release_name <- paste0("selenium-", version) 142 | } 143 | 144 | file_name <- paste0("selenium-server-", version, ".jar") 145 | 146 | if (!is.null(path)) { 147 | dir <- normalizePath(path, winslash = "/", mustWork = TRUE) 148 | } else if (temp) { 149 | dir <- tempfile(pattern = "file", tmpdir = tempdir()) 150 | dir.create(dir) 151 | } else { 152 | app_dir <- rappdirs::user_data_dir("selenium-server", "seleniumHQ", version = version) 153 | dir <- normalizePath(app_dir, winslash = "/", mustWork = FALSE) 154 | } 155 | 156 | full_path <- if (dir == "") { 157 | NULL 158 | } else { 159 | file.path(dir, file_name) 160 | } 161 | 162 | if (interactive && !rlang::is_interactive()) { 163 | rlang::abort(c( 164 | "An interactive session is required to download Selenium Server.", 165 | "Set `interactive` to `FALSE` to disable this warning." 166 | )) 167 | } 168 | 169 | if ((is.null(full_path) || !file.exists(full_path)) && interactive) { 170 | choices <- utils::menu( 171 | title = paste0("Should we download Selenium Server version ", version, " from GitHub?"), 172 | choices = c("Yes", "No"), 173 | ) 174 | if (choices != 1) { 175 | rlang::abort("Selenium Server not found.") 176 | } 177 | } 178 | 179 | if (!dir.exists(dir)) { 180 | dir.create(app_dir, recursive = TRUE) 181 | dir <- normalizePath(app_dir, winslash = "/", mustWork = FALSE) 182 | full_path <- file.path(dir, file_name) 183 | } 184 | 185 | if (!file.exists(full_path)) { 186 | download_server(full_path, file_name, release_name, verbose) 187 | } 188 | 189 | args <- c("-jar", full_path, "standalone") 190 | 191 | if (selenium_manager) { 192 | args <- c(args, "--selenium-manager", "true") 193 | } 194 | 195 | args <- c(args, "--host", host, "--port", port) 196 | 197 | args <- c(args, extra_args) 198 | 199 | SeleniumServer$new( 200 | java_check(), 201 | args = args, 202 | host = host, 203 | port = port, 204 | supervise = TRUE 205 | ) 206 | } 207 | 208 | #' A Selenium Server process 209 | #' 210 | #' @description 211 | #' A [processx::process] object representing a running Selenium Server. 212 | #' 213 | #' @export 214 | SeleniumServer <- R6::R6Class("SeleniumServer", 215 | inherit = processx::process, 216 | public = list( 217 | #' @description 218 | #' 219 | #' Create a new `SeleniumServer` object. It is recommended that you use 220 | #' the [selenium_server()] function instead. 221 | #' 222 | #' @param command,args,... Passed into 223 | #' [processx::process$new()][processx::process]. 224 | #' @param host The host that the Selenium server is running on. 225 | #' @param port The port that the Selenium server is using. 226 | #' 227 | #' @returns A new `SeleniumServer` object. 228 | initialize = function(command, 229 | args, 230 | host, 231 | port, 232 | ...) { 233 | private$.host <- host 234 | private$.port <- port 235 | 236 | super$initialize( 237 | command = command, 238 | args = args, 239 | ..., 240 | ) 241 | } 242 | ), 243 | active = list( 244 | #' @field host The host that the Selenium server is running on. 245 | host = function() private$.host, 246 | #' @field port The port that the Selenium server is using. 247 | port = function() private$.port 248 | ), 249 | private = list( 250 | .host = NULL, 251 | .port = NULL 252 | ) 253 | ) 254 | 255 | download_server <- function(path, file, name, verbose) { 256 | url <- paste0( 257 | "https://github.com/SeleniumHQ/selenium/releases/download/", 258 | name, 259 | "/", 260 | file 261 | ) 262 | 263 | utils::download.file(url, path, quiet = !verbose) 264 | file 265 | } 266 | 267 | get_latest_version_name <- function() { 268 | sel_env <- get_selenium_env() 269 | 270 | stored_name <- get_from_env("latest_version_name") 271 | if (!is.null(stored_name)) { 272 | return(stored_name) 273 | } 274 | 275 | req <- httr2::request("https://api.github.com/repos/seleniumHQ/selenium/releases/latest") 276 | req <- httr2::req_headers(req, "Accept" = "application/vnd.github+json") 277 | 278 | token <- if (is_installed("gitcreds")) { 279 | tryCatch( 280 | gitcreds::gitcreds_get(), 281 | error = function(e) NULL 282 | ) 283 | } else { 284 | NULL 285 | } 286 | 287 | if (!is.null(token)) { 288 | token <- paste("token", token$password) 289 | req <- httr2::req_headers(req, Authorization = token) 290 | } 291 | 292 | response <- httr2::req_perform(req) 293 | release <- httr2::resp_body_json(response) 294 | 295 | url_parts <- strsplit(release$html_url, split = "/")[[1]] 296 | 297 | url_parts[[length(url_parts)]] 298 | } 299 | 300 | get_version_from_files <- function(error) { 301 | dir <- rappdirs::user_data_dir("selenium-server", "seleniumHQ") 302 | dir <- normalizePath(dir, winslash = "/", mustWork = FALSE) 303 | if (!dir.exists(dir)) { 304 | rlang::abort("Could not make request to GitHub.", parent = error) 305 | } 306 | 307 | files <- list.dirs(dir, full.names = FALSE) 308 | versions <- numeric_version(files[files != ""]) 309 | version <- max((files), na.rm = TRUE) 310 | rlang::warn(c( 311 | "Github request failed: Could not determine latest version of Selenium Server.", 312 | "i" = paste0("Using latest downloaded version ", version, " instead.") 313 | )) 314 | version 315 | } 316 | 317 | java_check <- function() { 318 | java_home <- Sys.getenv("JAVA_HOME") 319 | if (nzchar(java_home)) { 320 | java <- file.path(java_home, "bin", "java") 321 | java_windows <- file.path(java_home, "bin", "java.exe") 322 | if (file.exists(java)) { 323 | return(java) 324 | } else if (file.exists(java_windows)) { 325 | return(java_windows) 326 | } 327 | } 328 | 329 | java <- Sys.which("java") 330 | if (identical(unname(java), "")) { 331 | rlang::abort("Java not found. Please install Java to use `selenium_server()`.") 332 | } 333 | java 334 | } 335 | 336 | find_using <- function(x, .f) { 337 | for (a in x) { 338 | if (.f(a)) { 339 | return(a) 340 | } 341 | } 342 | NULL 343 | } 344 | 345 | is_nonspecific_release <- function(x) { 346 | grepl("^selenium-([0-9]+\\.)+0$", x$name) 347 | } 348 | -------------------------------------------------------------------------------- /R/status.R: -------------------------------------------------------------------------------- 1 | #' Is a selenium server instance running? 2 | #' 3 | #' @description 4 | #' `wait_for_server()` takes a server process returned by [selenium_server()] 5 | #' and waits for it to respond to status requests. If it doesn't, then an 6 | #' error is thrown detailing any errors in the response and any error messages 7 | #' from the server. 8 | #' 9 | #' `selenium_server_available()` returns `TRUE` if a Selenium server is 10 | #' running on a given port and host. `wait_for_selenium_available()` waits 11 | #' for the Selenium server to become available for a given time, throwing an 12 | #' error if one does not. It is similar to `wait_for_server()` except that it 13 | #' works with servers not created by selenium. 14 | #' 15 | #' `get_server_status()`, when given a port and host, figures out whether a 16 | #' Selenium server instance is running, and if so, returns its status. This is 17 | #' used by `selenium_server_available()` to figure out if the server is 18 | #' running. 19 | #' 20 | #' @param server The process object returned by [selenium_server()]. 21 | #' @param host The host that the Selenium server is running on. This is 22 | #' usually 'localhost' (i.e. Your own machine). 23 | #' @param port The port that the Selenium server is using. 24 | #' @param max_time The amount of time to wait for the Selenium server to 25 | #' become available. 26 | #' @param error Whether to throw an error if the web request fails 27 | #' after the timeout is exceeded. If not, and we can't connect to a server, 28 | #' `FALSE` is returned. 29 | #' @param verbose Whether to print information about the web request that is 30 | #' sent. 31 | #' @param timeout How long to wait for a request to recieve a response before 32 | #' throwing an error. 33 | #' 34 | #' @returns 35 | #' `wait_for_server()` and `wait_for_selenium_available()` return `TRUE` if 36 | #' the server is ready to be connected to, and throw an error otherwise. 37 | #' 38 | #' `selenium_server_available()` returns `TRUE` if a Selenium server is 39 | #' running, and `FALSE` otherwise. 40 | #' 41 | #' `get_server_status()` returns a list that can (but may not always) contain 42 | #' the following fields: 43 | #' 44 | #' * `ready`: Whether the server is ready to be connected to. This should 45 | #' always be returned by the server. 46 | #' * `message`: A message about the status of the server. 47 | #' * `uptime`: How long the server has been running. 48 | #' * `nodes`: Information about the slots that the server can take. 49 | #' 50 | #' @examples 51 | #' \dontrun{ 52 | #' server <- selenium_server() 53 | #' 54 | #' wait_for_server(server) 55 | #' 56 | #' get_server_status() 57 | #' 58 | #' selenium_server_available() 59 | #' 60 | #' wait_for_selenium_available() 61 | #' } 62 | #' 63 | #' @export 64 | wait_for_server <- function(server, 65 | max_time = 60, 66 | error = TRUE, 67 | verbose = FALSE, 68 | timeout = 20) { 69 | check_class(server, "process") 70 | check_number_decimal(timeout) 71 | check_number_whole(max_time) 72 | check_bool(verbose) 73 | check_bool(error) 74 | 75 | result <- wait_for_status( 76 | max_time, 77 | host = server$host, 78 | port = server$port, 79 | verbose = verbose, 80 | timeout = timeout 81 | ) 82 | 83 | if (isTRUE(result)) { 84 | return(TRUE) 85 | } 86 | 87 | if (error) { 88 | base_message <- "Timed out waiting for selenium server to start" 89 | 90 | parent <- if (rlang::is_error(result)) result else NULL 91 | 92 | rlang::abort(base_message, parent = parent) 93 | } 94 | 95 | FALSE 96 | } 97 | 98 | #' @rdname wait_for_server 99 | #' 100 | #' @export 101 | selenium_server_available <- function(port = 4444L, host = "localhost", verbose = FALSE, timeout = 20) { 102 | check_number_whole(port) 103 | check_string(host) 104 | check_bool(verbose) 105 | 106 | tryCatch( 107 | isTRUE(get_server_status(port = port, host = host, verbose = verbose)$ready), 108 | error = function(e) FALSE 109 | ) 110 | } 111 | 112 | #' @rdname wait_for_server 113 | #' 114 | #' 115 | #' @export 116 | wait_for_selenium_available <- function(max_time = 60, 117 | host = "localhost", 118 | port = 4444L, 119 | error = TRUE, 120 | verbose = FALSE, 121 | timeout = 20) { 122 | check_number_decimal(timeout) 123 | check_number_whole(port) 124 | check_string(host) 125 | check_bool(verbose) 126 | check_bool(error) 127 | 128 | result <- wait_for_status(max_time, host, port, verbose, timeout) 129 | 130 | if (isTRUE(result)) { 131 | return(TRUE) 132 | } 133 | 134 | if (error) { 135 | parent <- if (rlang::is_error(result)) result else NULL 136 | 137 | rlang::abort("Timed out waiting for selenium server to start", parent = parent) 138 | } 139 | 140 | FALSE 141 | } 142 | 143 | 144 | wait_for_status <- function(max_time, host, port, verbose, timeout) { 145 | end <- Sys.time() + timeout 146 | 147 | result <- rlang::try_fetch( 148 | get_server_status(host = host, port = port, verbose = verbose, timeout = timeout), 149 | error = identity 150 | ) 151 | 152 | if (!rlang::is_error(result) && isTRUE(result$ready)) { 153 | return(TRUE) 154 | } 155 | 156 | while (Sys.time() <= end) { 157 | result <- rlang::try_fetch( 158 | get_server_status(port = port, host = host, verbose = verbose, timeout = timeout), 159 | error = identity 160 | ) 161 | 162 | if (!rlang::is_error(result) && isTRUE(result$ready)) { 163 | return(TRUE) 164 | } 165 | } 166 | 167 | if (rlang::is_error(result)) { 168 | return(result) 169 | } 170 | 171 | FALSE 172 | } 173 | 174 | get_status <- function(req, verbose = FALSE, timeout = 20) { 175 | req <- req_command(req, "Status") 176 | response <- req_perform_selenium(req, verbose = verbose, timeout = timeout) 177 | httr2::resp_body_json(response)$value 178 | } 179 | 180 | #' @rdname wait_for_server 181 | #' 182 | #' @export 183 | get_server_status <- function(host = "localhost", port = 4444L, verbose = FALSE, timeout = 20) { 184 | check_number_whole(port) 185 | check_string(host) 186 | check_bool(verbose) 187 | 188 | url <- sprintf("http://%s:%s", host, port) 189 | req <- httr2::request(url) 190 | get_status(req, verbose = verbose, timeout = timeout) 191 | } 192 | -------------------------------------------------------------------------------- /R/sysdata.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashbythorpe/selenium-r/df0e5fd83b5d308ca3fddc88af1b2da0e3dccff3/R/sysdata.rda -------------------------------------------------------------------------------- /R/utils-checks.R: -------------------------------------------------------------------------------- 1 | check_list <- function(x, 2 | ..., 3 | allow_na = FALSE, 4 | allow_null = FALSE, 5 | arg = caller_arg(x), 6 | call = caller_env()) { 7 | if (!missing(x) && (is.list(x) || (allow_null && is.null(x)))) { 8 | return(invisible(NULL)) 9 | } 10 | 11 | stop_input_type( 12 | x, 13 | c("a list"), 14 | ..., 15 | allow_na = allow_na, 16 | allow_null = allow_null, 17 | arg = arg, 18 | call = call 19 | ) 20 | } 21 | 22 | check_class <- function(x, cls, ..., allow_null = FALSE, arg = rlang::caller_arg(x), call = rlang::caller_env()) { 23 | what <- paste0("a <", cls, "> object or `NULL`") 24 | 25 | if (allow_null) { 26 | if (!is.null(x) && !inherits_any(x, cls)) { 27 | stop_input_type(x, what, ..., arg = arg, allow_null = allow_null, call = call) 28 | } 29 | } else { 30 | if (!inherits_any(x, cls)) { 31 | stop_input_type(x, what, ..., allow_null = allow_null, arg = arg, call = call) 32 | } 33 | } 34 | } 35 | 36 | check_char <- function(x, 37 | ..., 38 | allow_na = FALSE, 39 | allow_null = FALSE, 40 | arg = caller_arg(x), 41 | call = caller_env()) { 42 | check_string(x, ..., allow_na = allow_na, allow_null = allow_null, arg = arg, call = call) 43 | 44 | if (nchar(x) == 1) { 45 | return(invisible(NULL)) 46 | } 47 | 48 | stop_input_type( 49 | x, 50 | "a single character", 51 | ..., 52 | allow_na = allow_na, 53 | allow_null = allow_null, 54 | arg = arg, 55 | call = call 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | combine_lists <- function(x, y, depth = 1) { 2 | if (is.function(y)) { 3 | return(y(x)) 4 | } else if (depth == 0 || is.null(x)) { 5 | return(y) 6 | } 7 | 8 | for (i in seq_along(y)) { 9 | nm <- names(y)[i] 10 | x[[nm]] <- combine_lists(x[[nm]], y[[i]], depth - 1) 11 | } 12 | 13 | x 14 | } 15 | 16 | named_list <- function() { 17 | res <- list() 18 | names(res) <- character() 19 | res 20 | } 21 | 22 | compact <- function(x) { 23 | x[!vapply(x, is_empty, logical(1))] 24 | } 25 | 26 | is_empty <- function(x) length(x) == 0 27 | 28 | web_element_id <- "element-6066-11e4-a52e-4f735466cecf" 29 | 30 | shadow_element_id <- "shadow-6066-11e4-a52e-4f735466cecf" 31 | 32 | to_sentence_case <- function(x) { 33 | paste0(toupper(substring(x, 1, 1)), substring(x, 2)) 34 | } 35 | 36 | rand_id <- function() { 37 | as.character(round(stats::runif(1, min = 0, max = 1000000))) 38 | } 39 | 40 | merge_lists <- function(x, y) { 41 | for (i in seq_along(y)) { 42 | x[[names(y)[i]]] <- y[[i]] 43 | } 44 | 45 | x 46 | } 47 | 48 | env_var_is_true <- function(x) { 49 | isTRUE(as.logical(Sys.getenv(x, "false"))) 50 | } 51 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | available <- selenium::selenium_server_available() 9 | knitr::opts_chunk$set( 10 | collapse = TRUE, 11 | comment = "#>", 12 | fig.path = "man/figures/README-", 13 | out.width = "100%", 14 | eval = available 15 | ) 16 | ``` 17 | 18 | ```{r eval = !available, echo = FALSE, comment = NA} 19 | if (!available) { 20 | message("Selenium server is not available.") 21 | } 22 | ``` 23 | 24 | # selenium 25 | 26 | 27 | [![R-CMD-check](https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml) 28 | [![CRAN status](https://www.r-pkg.org/badges/version/selenium)](https://CRAN.R-project.org/package=selenium) 29 | 30 | 31 | selenium is a tool for the automation of web browsers. It is a low-level interface 32 | to the [WebDriver](https://w3c.github.io/webdriver/) specification, and an up-to-date 33 | alternative to [RSelenium](https://github.com/ropensci/RSelenium). 34 | 35 | ## Installation 36 | 37 | ``` {r eval = FALSE} 38 | # Install selenider from CRAN 39 | install.packages("selenium") 40 | 41 | # Or the development version from Github 42 | # install.packages("pak") 43 | pak::pak("ashbythorpe/selenium-r") 44 | ``` 45 | 46 | However, you must also have a selenium server installed and running (see below). 47 | 48 | ## Starting the server 49 | A selenium instance consists of two parts: the client and the server. 50 | The selenium package *only provides the client*. This means that you 51 | have to start the server yourself. 52 | 53 | To do this you must: 54 | 55 | * Install a browser that you want to automate (e.g. Chrome, Firefox, Edge). 56 | * Download [Java](https://www.oracle.com/java/technologies/downloads/) (you need Java 11 or higher). 57 | 58 | There are many different ways to download and start the server, one of which 59 | is provided by selenium: 60 | 61 | ```{r setup} 62 | library(selenium) 63 | ``` 64 | 65 | ```{r eval = FALSE} 66 | server <- selenium_server() 67 | ``` 68 | 69 | This will download the latest version of the server and start it. 70 | 71 | By default, the server file will be stored in a temporary directory, meaning it 72 | will be deleted when the session is closed. If you want the server to persist, 73 | meaning that you don't have to re-download the server each time, you can use 74 | the `temp` argument: 75 | 76 | ``` {r eval = FALSE} 77 | server <- selenium_server(temp = FALSE) 78 | ``` 79 | 80 | You can also do this manually if you want: 81 | 82 | 1. Download the latest `.jar` file for Selenium Server. Do this by navigating to 83 | the latest GitHub release page (), 84 | scrolling down to the **Assets** section, and downloading the file named 85 | `selenium-server-standalone-.jar` (with `` being the latest release version). 86 | 2. Make sure you are in the same directory as the file you downloaded. 87 | 3. In the terminal, run `java -jar selenium-server-standalone-.jar standalone --selenium-manager true`, 88 | replacing `` with the version number that you downloaded. This will download 89 | any drivers you need to communicate with the server and the browser, and start the server. 90 | 91 | There are a few other ways of starting Selenium Server: 92 | 93 | * Using docker to start the server. See . 94 | This is recommended in a non-interactive context (e.g. GitHub Actions). 95 | * Using the `wdman` package to start the server from R, using `wdman::selenium()`. Note 96 | that at the time of writing, this package does not work with the latest version of 97 | Chrome. 98 | 99 | ## Waiting for the server to be online 100 | 101 | The Selenium server won't be ready to be used immediately. If you used 102 | `selenium_server()` to create your server, you can pass it into 103 | `wait_for_server()`: 104 | 105 | ``` {r eval = FALSE} 106 | wait_for_server(server) 107 | ``` 108 | 109 | You can also use `server$read_output()` and `server$read_error()` 110 | 111 | If you used a different method to create your server, use 112 | `wait_for_selenium_available()` instead. 113 | 114 | ``` {r eval = FALSE} 115 | wait_for_selenium_available() 116 | ``` 117 | 118 | If any point in this process produces an error or doesn't work, please see the 119 | [Debugging Selenium](https://ashbythorpe.github.io/selenium-r/articles/debugging.html) 120 | article for more information. 121 | 122 | ## Starting the client 123 | 124 | Client sessions can be started using `SeleniumSession$new()` 125 | ``` {r eval = FALSE} 126 | session <- SeleniumSession$new() 127 | ``` 128 | 129 | By default, this will connect to Firefox, but you can use the `browser` argument to specify 130 | a different browser if you like. 131 | 132 | ```{r session} 133 | session <- SeleniumSession$new(browser = "chrome") 134 | ``` 135 | 136 | ## Usage 137 | 138 | Once the session has been successfully started, you can use the session 139 | object to control the browser. Here, we dynamically navigate through 140 | the R project homepage. Remember to close the session and the server process 141 | when you are done. 142 | 143 | ```{r example} 144 | session$navigate("https://www.r-project.org/") 145 | session$ 146 | find_element(using = "css selector", value = ".row")$ 147 | find_element(using = "css selector", value = "ul")$ 148 | find_element(using = "css selector", value = "a")$ 149 | click() 150 | 151 | session$ 152 | find_element(using = "css selector", value = ".row")$ 153 | find_elements(using = "css selector", value = "div")[[2]]$ 154 | find_element(using = "css selector", value = "p")$ 155 | get_text() 156 | 157 | session$close() 158 | ``` 159 | ``` {r eval = FALSE} 160 | server$kill() 161 | ``` 162 | 163 | For a more detailed introduction to using selenium, see the 164 | [Getting Started](https://ashbythorpe.github.io/selenium-r/articles/selenium.html) 165 | article. 166 | 167 | Note that selenium is low-level and mainly aimed towards developers. If you are 168 | wanting to use browser automation for web scraping or testing, you may want to 169 | take a look at [selenider](https://github.com/ashbythorpe/selenider) instead. 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # selenium 5 | 6 | 7 | 8 | [![R-CMD-check](https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml) 9 | [![CRAN 10 | status](https://www.r-pkg.org/badges/version/selenium)](https://CRAN.R-project.org/package=selenium) 11 | 12 | 13 | selenium is a tool for the automation of web browsers. It is a low-level 14 | interface to the [WebDriver](https://w3c.github.io/webdriver/) 15 | specification, and an up-to-date alternative to 16 | [RSelenium](https://github.com/ropensci/RSelenium). 17 | 18 | ## Installation 19 | 20 | ``` r 21 | # Install selenider from CRAN 22 | install.packages("selenium") 23 | 24 | # Or the development version from Github 25 | # install.packages("pak") 26 | pak::pak("ashbythorpe/selenium-r") 27 | ``` 28 | 29 | However, you must also have a selenium server installed and running (see 30 | below). 31 | 32 | ## Starting the server 33 | 34 | A selenium instance consists of two parts: the client and the server. 35 | The selenium package *only provides the client*. This means that you 36 | have to start the server yourself. 37 | 38 | To do this you must: 39 | 40 | - Install a browser that you want to automate (e.g. Chrome, Firefox, 41 | Edge). 42 | - Download [Java](https://www.oracle.com/java/technologies/downloads/) 43 | (you need Java 11 or higher). 44 | 45 | There are many different ways to download and start the server, one of 46 | which is provided by selenium: 47 | 48 | ``` r 49 | library(selenium) 50 | ``` 51 | 52 | ``` r 53 | server <- selenium_server() 54 | ``` 55 | 56 | This will download the latest version of the server and start it. 57 | 58 | By default, the server file will be stored in a temporary directory, 59 | meaning it will be deleted when the session is closed. If you want the 60 | server to persist, meaning that you don’t have to re-download the server 61 | each time, you can use the `temp` argument: 62 | 63 | ``` r 64 | server <- selenium_server(temp = FALSE) 65 | ``` 66 | 67 | You can also do this manually if you want: 68 | 69 | 1. Download the latest `.jar` file for Selenium Server. Do this by 70 | navigating to the latest GitHub release page 71 | (), 72 | scrolling down to the **Assets** section, and downloading the file 73 | named `selenium-server-standalone-.jar` (with `` 74 | being the latest release version). 75 | 2. Make sure you are in the same directory as the file you downloaded. 76 | 3. In the terminal, run 77 | `java -jar selenium-server-standalone-.jar standalone --selenium-manager true`, 78 | replacing `` with the version number that you downloaded. 79 | This will download any drivers you need to communicate with the 80 | server and the browser, and start the server. 81 | 82 | There are a few other ways of starting Selenium Server: 83 | 84 | - Using docker to start the server. See 85 | . This is recommended 86 | in a non-interactive context (e.g. GitHub Actions). 87 | - Using the `wdman` package to start the server from R, using 88 | `wdman::selenium()`. Note that at the time of writing, this package 89 | does not work with the latest version of Chrome. 90 | 91 | ## Waiting for the server to be online 92 | 93 | The Selenium server won’t be ready to be used immediately. If you used 94 | `selenium_server()` to create your server, you can pass it into 95 | `wait_for_server()`: 96 | 97 | ``` r 98 | wait_for_server(server) 99 | ``` 100 | 101 | You can also use `server$read_output()` and `server$read_error()` 102 | 103 | If you used a different method to create your server, use 104 | `wait_for_selenium_available()` instead. 105 | 106 | ``` r 107 | wait_for_selenium_available() 108 | ``` 109 | 110 | If any point in this process produces an error or doesn’t work, please 111 | see the [Debugging 112 | Selenium](https://ashbythorpe.github.io/selenium-r/articles/debugging.html) 113 | article for more information. 114 | 115 | ## Starting the client 116 | 117 | Client sessions can be started using `SeleniumSession$new()` 118 | 119 | ``` r 120 | session <- SeleniumSession$new() 121 | ``` 122 | 123 | By default, this will connect to Firefox, but you can use the `browser` 124 | argument to specify a different browser if you like. 125 | 126 | ``` r 127 | session <- SeleniumSession$new(browser = "chrome") 128 | ``` 129 | 130 | ## Usage 131 | 132 | Once the session has been successfully started, you can use the session 133 | object to control the browser. Here, we dynamically navigate through the 134 | R project homepage. Remember to close the session and the server process 135 | when you are done. 136 | 137 | ``` r 138 | session$navigate("https://www.r-project.org/") 139 | session$ 140 | find_element(using = "css selector", value = ".row")$ 141 | find_element(using = "css selector", value = "ul")$ 142 | find_element(using = "css selector", value = "a")$ 143 | click() 144 | 145 | session$ 146 | find_element(using = "css selector", value = ".row")$ 147 | find_elements(using = "css selector", value = "div")[[2]]$ 148 | find_element(using = "css selector", value = "p")$ 149 | get_text() 150 | #> [1] "" 151 | 152 | session$close() 153 | ``` 154 | 155 | ``` r 156 | server$kill() 157 | ``` 158 | 159 | For a more detailed introduction to using selenium, see the [Getting 160 | Started](https://ashbythorpe.github.io/selenium-r/articles/selenium.html) 161 | article. 162 | 163 | Note that selenium is low-level and mainly aimed towards developers. If 164 | you are wanting to use browser automation for web scraping or testing, 165 | you may want to take a look at 166 | [selenider](https://github.com/ashbythorpe/selenider) instead. 167 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://ashbythorpe.github.io/selenium-r/ 2 | template: 3 | bootstrap: 5 4 | 5 | development: 6 | mode: auto 7 | 8 | reference: 9 | - title: Start a Selenium Server 10 | desc: Start a Selenium server instance and check that it is running. 11 | - contents: 12 | - selenium_server 13 | - selenium_server_available 14 | - SeleniumServer 15 | - title: Start a session 16 | desc: R6 classes for automating a web page. 17 | - contents: 18 | - SeleniumSession 19 | - WebElement 20 | - ShadowRoot 21 | - title: Browser options 22 | desc: Configure browser options for a session. 23 | - contents: 24 | - chrome_options 25 | - title: Keys 26 | desc: Send keys to the page. 27 | - contents: 28 | - keys 29 | - key_chord 30 | - title: Actions 31 | desc: Perform custom actions. 32 | - contents: 33 | - actions_stream 34 | - actions_pause 35 | - actions_press 36 | - actions_mousedown 37 | - actions_scroll 38 | -------------------------------------------------------------------------------- /codemeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://doi.org/10.5063/schema/codemeta-2.0", 3 | "@type": "SoftwareSourceCode", 4 | "identifier": "selenium", 5 | "description": "An implementation of 'W3C WebDriver 2.0' (), allowing interaction with a 'Selenium Server' () instance from 'R'. Allows a web browser to be automated from 'R'.", 6 | "name": "selenium: Low-Level Browser Automation Interface", 7 | "relatedLink": ["https://ashbythorpe.github.io/selenium-r/", "https://CRAN.R-project.org/package=selenium"], 8 | "codeRepository": "https://github.com/ashbythorpe/selenium-r", 9 | "issueTracker": "https://github.com/ashbythorpe/selenium-r/issues", 10 | "license": "https://spdx.org/licenses/MIT", 11 | "version": "0.1.4", 12 | "programmingLanguage": { 13 | "@type": "ComputerLanguage", 14 | "name": "R", 15 | "url": "https://r-project.org" 16 | }, 17 | "runtimePlatform": "R version 4.4.1 (2024-06-14)", 18 | "provider": { 19 | "@id": "https://cran.r-project.org", 20 | "@type": "Organization", 21 | "name": "Comprehensive R Archive Network (CRAN)", 22 | "url": "https://cran.r-project.org" 23 | }, 24 | "author": [ 25 | { 26 | "@type": "Person", 27 | "givenName": "Ashby", 28 | "familyName": "Thorpe", 29 | "email": "ashbythorpe@gmail.com", 30 | "@id": "https://orcid.org/0000-0003-3106-099X" 31 | } 32 | ], 33 | "copyrightHolder": [ 34 | { 35 | "@type": "Person", 36 | "givenName": "Ashby", 37 | "familyName": "Thorpe", 38 | "email": "ashbythorpe@gmail.com", 39 | "@id": "https://orcid.org/0000-0003-3106-099X" 40 | } 41 | ], 42 | "maintainer": [ 43 | { 44 | "@type": "Person", 45 | "givenName": "Ashby", 46 | "familyName": "Thorpe", 47 | "email": "ashbythorpe@gmail.com", 48 | "@id": "https://orcid.org/0000-0003-3106-099X" 49 | } 50 | ], 51 | "softwareSuggestions": [ 52 | { 53 | "@type": "SoftwareApplication", 54 | "identifier": "gitcreds", 55 | "name": "gitcreds", 56 | "provider": { 57 | "@id": "https://cran.r-project.org", 58 | "@type": "Organization", 59 | "name": "Comprehensive R Archive Network (CRAN)", 60 | "url": "https://cran.r-project.org" 61 | }, 62 | "sameAs": "https://CRAN.R-project.org/package=gitcreds" 63 | }, 64 | { 65 | "@type": "SoftwareApplication", 66 | "identifier": "testthat", 67 | "name": "testthat", 68 | "version": ">= 3.0.0", 69 | "provider": { 70 | "@id": "https://cran.r-project.org", 71 | "@type": "Organization", 72 | "name": "Comprehensive R Archive Network (CRAN)", 73 | "url": "https://cran.r-project.org" 74 | }, 75 | "sameAs": "https://CRAN.R-project.org/package=testthat" 76 | }, 77 | { 78 | "@type": "SoftwareApplication", 79 | "identifier": "withr", 80 | "name": "withr", 81 | "provider": { 82 | "@id": "https://cran.r-project.org", 83 | "@type": "Organization", 84 | "name": "Comprehensive R Archive Network (CRAN)", 85 | "url": "https://cran.r-project.org" 86 | }, 87 | "sameAs": "https://CRAN.R-project.org/package=withr" 88 | }, 89 | { 90 | "@type": "SoftwareApplication", 91 | "identifier": "xml2", 92 | "name": "xml2", 93 | "provider": { 94 | "@id": "https://cran.r-project.org", 95 | "@type": "Organization", 96 | "name": "Comprehensive R Archive Network (CRAN)", 97 | "url": "https://cran.r-project.org" 98 | }, 99 | "sameAs": "https://CRAN.R-project.org/package=xml2" 100 | } 101 | ], 102 | "softwareRequirements": { 103 | "1": { 104 | "@type": "SoftwareApplication", 105 | "identifier": "R", 106 | "name": "R", 107 | "version": ">= 2.10" 108 | }, 109 | "2": { 110 | "@type": "SoftwareApplication", 111 | "identifier": "base64enc", 112 | "name": "base64enc", 113 | "provider": { 114 | "@id": "https://cran.r-project.org", 115 | "@type": "Organization", 116 | "name": "Comprehensive R Archive Network (CRAN)", 117 | "url": "https://cran.r-project.org" 118 | }, 119 | "sameAs": "https://CRAN.R-project.org/package=base64enc" 120 | }, 121 | "3": { 122 | "@type": "SoftwareApplication", 123 | "identifier": "httr2", 124 | "name": "httr2", 125 | "provider": { 126 | "@id": "https://cran.r-project.org", 127 | "@type": "Organization", 128 | "name": "Comprehensive R Archive Network (CRAN)", 129 | "url": "https://cran.r-project.org" 130 | }, 131 | "sameAs": "https://CRAN.R-project.org/package=httr2" 132 | }, 133 | "4": { 134 | "@type": "SoftwareApplication", 135 | "identifier": "jsonlite", 136 | "name": "jsonlite", 137 | "provider": { 138 | "@id": "https://cran.r-project.org", 139 | "@type": "Organization", 140 | "name": "Comprehensive R Archive Network (CRAN)", 141 | "url": "https://cran.r-project.org" 142 | }, 143 | "sameAs": "https://CRAN.R-project.org/package=jsonlite" 144 | }, 145 | "5": { 146 | "@type": "SoftwareApplication", 147 | "identifier": "lifecycle", 148 | "name": "lifecycle", 149 | "provider": { 150 | "@id": "https://cran.r-project.org", 151 | "@type": "Organization", 152 | "name": "Comprehensive R Archive Network (CRAN)", 153 | "url": "https://cran.r-project.org" 154 | }, 155 | "sameAs": "https://CRAN.R-project.org/package=lifecycle" 156 | }, 157 | "6": { 158 | "@type": "SoftwareApplication", 159 | "identifier": "processx", 160 | "name": "processx", 161 | "provider": { 162 | "@id": "https://cran.r-project.org", 163 | "@type": "Organization", 164 | "name": "Comprehensive R Archive Network (CRAN)", 165 | "url": "https://cran.r-project.org" 166 | }, 167 | "sameAs": "https://CRAN.R-project.org/package=processx" 168 | }, 169 | "7": { 170 | "@type": "SoftwareApplication", 171 | "identifier": "R6", 172 | "name": "R6", 173 | "provider": { 174 | "@id": "https://cran.r-project.org", 175 | "@type": "Organization", 176 | "name": "Comprehensive R Archive Network (CRAN)", 177 | "url": "https://cran.r-project.org" 178 | }, 179 | "sameAs": "https://CRAN.R-project.org/package=R6" 180 | }, 181 | "8": { 182 | "@type": "SoftwareApplication", 183 | "identifier": "rappdirs", 184 | "name": "rappdirs", 185 | "provider": { 186 | "@id": "https://cran.r-project.org", 187 | "@type": "Organization", 188 | "name": "Comprehensive R Archive Network (CRAN)", 189 | "url": "https://cran.r-project.org" 190 | }, 191 | "sameAs": "https://CRAN.R-project.org/package=rappdirs" 192 | }, 193 | "9": { 194 | "@type": "SoftwareApplication", 195 | "identifier": "rlang", 196 | "name": "rlang", 197 | "version": ">= 1.1.0", 198 | "provider": { 199 | "@id": "https://cran.r-project.org", 200 | "@type": "Organization", 201 | "name": "Comprehensive R Archive Network (CRAN)", 202 | "url": "https://cran.r-project.org" 203 | }, 204 | "sameAs": "https://CRAN.R-project.org/package=rlang" 205 | }, 206 | "SystemRequirements": null 207 | }, 208 | "fileSize": "309.387KB", 209 | "releaseNotes": "https://github.com/ashbythorpe/selenium-r/blob/master/NEWS.md", 210 | "readme": "https://github.com/ashbythorpe/selenium-r/blob/main/README.md", 211 | "contIntegration": "https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml" 212 | } 213 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## R CMD CHECK results 2 | 3 | 0 errors | 0 warnings | 0 notes 4 | 5 | ## revdepcheck results 6 | 7 | We checked 1 reverse dependencies (0 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. 8 | 9 | - We saw 0 new problems 10 | - We failed to check 0 packages 11 | -------------------------------------------------------------------------------- /data-raw/internal.R: -------------------------------------------------------------------------------- 1 | ## code to prepare `commands` dataset goes here 2 | library(rvest) 3 | library(dplyr) 4 | 5 | session <- read_html("https://w3c.github.io/webdriver") 6 | 7 | table <- session |> 8 | html_element("#endpoints") |> 9 | html_element("table.simple") |> 10 | html_table() 11 | 12 | commands <- mapply(function(x, y) list(method = x, url = y), table$Method, table$`URI Template`, SIMPLIFY = FALSE) 13 | names(commands) <- table$Command 14 | 15 | commands[["Element Displayed"]] <- list( 16 | method = "GET", 17 | url = "/session/{session id}/element/{element id}/displayed" 18 | ) 19 | 20 | usethis::use_data(commands, overwrite = TRUE, internal = TRUE) 21 | -------------------------------------------------------------------------------- /man/SeleniumServer.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/server.R 3 | \name{SeleniumServer} 4 | \alias{SeleniumServer} 5 | \title{A Selenium Server process} 6 | \value{ 7 | A new \code{SeleniumServer} object. 8 | } 9 | \description{ 10 | A \link[processx:process]{processx::process} object representing a running Selenium Server. 11 | } 12 | \section{Super class}{ 13 | \code{\link[processx:process]{processx::process}} -> \code{SeleniumServer} 14 | } 15 | \section{Active bindings}{ 16 | \if{html}{\out{
}} 17 | \describe{ 18 | \item{\code{host}}{The host that the Selenium server is running on.} 19 | 20 | \item{\code{port}}{The port that the Selenium server is using.} 21 | } 22 | \if{html}{\out{
}} 23 | } 24 | \section{Methods}{ 25 | \subsection{Public methods}{ 26 | \itemize{ 27 | \item \href{#method-SeleniumServer-new}{\code{SeleniumServer$new()}} 28 | \item \href{#method-SeleniumServer-clone}{\code{SeleniumServer$clone()}} 29 | } 30 | } 31 | \if{html}{\out{ 32 |
Inherited methods 33 | 84 |
85 | }} 86 | \if{html}{\out{
}} 87 | \if{html}{\out{}} 88 | \if{latex}{\out{\hypertarget{method-SeleniumServer-new}{}}} 89 | \subsection{Method \code{new()}}{ 90 | Create a new \code{SeleniumServer} object. It is recommended that you use 91 | the \code{\link[=selenium_server]{selenium_server()}} function instead. 92 | \subsection{Usage}{ 93 | \if{html}{\out{
}}\preformatted{SeleniumServer$new(command, args, host, port, ...)}\if{html}{\out{
}} 94 | } 95 | 96 | \subsection{Arguments}{ 97 | \if{html}{\out{
}} 98 | \describe{ 99 | \item{\code{command, args, ...}}{Passed into 100 | \link[processx:process]{processx::process$new()}.} 101 | 102 | \item{\code{host}}{The host that the Selenium server is running on.} 103 | 104 | \item{\code{port}}{The port that the Selenium server is using.} 105 | } 106 | \if{html}{\out{
}} 107 | } 108 | } 109 | \if{html}{\out{
}} 110 | \if{html}{\out{}} 111 | \if{latex}{\out{\hypertarget{method-SeleniumServer-clone}{}}} 112 | \subsection{Method \code{clone()}}{ 113 | The objects of this class are cloneable with this method. 114 | \subsection{Usage}{ 115 | \if{html}{\out{
}}\preformatted{SeleniumServer$clone(deep = FALSE)}\if{html}{\out{
}} 116 | } 117 | 118 | \subsection{Arguments}{ 119 | \if{html}{\out{
}} 120 | \describe{ 121 | \item{\code{deep}}{Whether to make a deep clone.} 122 | } 123 | \if{html}{\out{
}} 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /man/ShadowRoot.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/element.R 3 | \name{ShadowRoot} 4 | \alias{ShadowRoot} 5 | \title{Create a shadow root} 6 | \description{ 7 | A shadow DOM is a self-contained DOM tree, contained within another DOM 8 | tree. A shadow root is an element that contains a DOM subtree. This class 9 | represents a shadow root object, allowing you to select elements within 10 | the shadow root. 11 | } 12 | \examples{ 13 | 14 | ## ------------------------------------------------ 15 | ## Method `ShadowRoot$new` 16 | ## ------------------------------------------------ 17 | 18 | \dontrun{ 19 | session <- SeleniumSession$new() 20 | 21 | # Let's create our own Shadow Root using JavaScript 22 | session$execute_script(" 23 | const div = document.createElement('div'); 24 | document.body.appendChild(div); 25 | div.attachShadow({mode: 'open'}); 26 | ") 27 | 28 | element <- session$find_element(using = "css selector", value = "div") 29 | 30 | element$shadow_root() 31 | 32 | session$close() 33 | } 34 | 35 | ## ------------------------------------------------ 36 | ## Method `ShadowRoot$find_element` 37 | ## ------------------------------------------------ 38 | 39 | \dontrun{ 40 | session <- SeleniumSession$new() 41 | 42 | # Let's create our own Shadow Root using JavaScript 43 | session$execute_script(" 44 | const div = document.createElement('div'); 45 | document.body.appendChild(div); 46 | const shadowRoot = div.attachShadow({mode: 'open'}); 47 | const span = document.createElement('span'); 48 | span.textContent = 'Hello'; 49 | shadowRoot.appendChild(span); 50 | ") 51 | 52 | element <- session$find_element(using = "css selector", value = "div") 53 | 54 | shadow_root <- element$shadow_root() 55 | 56 | shadow_root$find_element(using = "css selector", value = "span") 57 | 58 | session$close() 59 | } 60 | 61 | ## ------------------------------------------------ 62 | ## Method `ShadowRoot$find_elements` 63 | ## ------------------------------------------------ 64 | 65 | \dontrun{ 66 | session <- SeleniumSession$new() 67 | 68 | # Let's create our own Shadow Root using JavaScript 69 | session$execute_script(" 70 | const div = document.createElement('div'); 71 | document.body.appendChild(div); 72 | const shadowRoot = div.attachShadow({mode: 'open'}); 73 | const span = document.createElement('span'); 74 | span.textContent = 'Hello'; 75 | shadowRoot.appendChild(span); 76 | const p = document.createElement('p'); 77 | p.textContent = 'Me too!'; 78 | shadowRoot.appendChild(p); 79 | ") 80 | 81 | element <- session$find_element(using = "css selector", value = "div") 82 | 83 | shadow_root <- element$shadow_root() 84 | 85 | shadow_root$find_elements(using = "css selector", value = "*") 86 | 87 | session$close() 88 | } 89 | 90 | ## ------------------------------------------------ 91 | ## Method `ShadowRoot$toJSON` 92 | ## ------------------------------------------------ 93 | 94 | \dontrun{ 95 | session <- SeleniumSession$new() 96 | 97 | # Let's create our own Shadow Root using JavaScript 98 | session$execute_script(" 99 | const div = document.createElement('div'); 100 | document.body.appendChild(div); 101 | div.attachShadow({mode: 'open'}); 102 | ") 103 | 104 | element <- session$find_element(using = "css selector", value = "div") 105 | 106 | shadow_root <- element$shadow_root() 107 | 108 | result <- shadow_root$toJSON() 109 | 110 | result 111 | 112 | jsonlite::toJSON(result, auto_unbox = TRUE) 113 | 114 | session$close() 115 | } 116 | } 117 | \section{Public fields}{ 118 | \if{html}{\out{
}} 119 | \describe{ 120 | \item{\code{id}}{The id of the shadow root.} 121 | } 122 | \if{html}{\out{
}} 123 | } 124 | \section{Methods}{ 125 | \subsection{Public methods}{ 126 | \itemize{ 127 | \item \href{#method-ShadowRoot-new}{\code{ShadowRoot$new()}} 128 | \item \href{#method-ShadowRoot-find_element}{\code{ShadowRoot$find_element()}} 129 | \item \href{#method-ShadowRoot-find_elements}{\code{ShadowRoot$find_elements()}} 130 | \item \href{#method-ShadowRoot-toJSON}{\code{ShadowRoot$toJSON()}} 131 | \item \href{#method-ShadowRoot-clone}{\code{ShadowRoot$clone()}} 132 | } 133 | } 134 | \if{html}{\out{
}} 135 | \if{html}{\out{}} 136 | \if{latex}{\out{\hypertarget{method-ShadowRoot-new}{}}} 137 | \subsection{Method \code{new()}}{ 138 | Initialize a new \code{ShadowRoot} object. This should not be called 139 | manually: instead use \link[=WebElement]{WebElement$shadow_root()}, or 140 | \link[=SeleniumSession]{SeleniumSession$create_shadow_root()}. 141 | \subsection{Usage}{ 142 | \if{html}{\out{
}}\preformatted{ShadowRoot$new(session_id, req, verbose, id)}\if{html}{\out{
}} 143 | } 144 | 145 | \subsection{Arguments}{ 146 | \if{html}{\out{
}} 147 | \describe{ 148 | \item{\code{session_id}}{The id of the session.} 149 | 150 | \item{\code{req, verbose}}{Private fields of a \link{SeleniumSession} object.} 151 | 152 | \item{\code{id}}{The id of the shadow root.} 153 | } 154 | \if{html}{\out{
}} 155 | } 156 | \subsection{Returns}{ 157 | A \code{ShadowRoot} object. 158 | } 159 | \subsection{Examples}{ 160 | \if{html}{\out{
}} 161 | \preformatted{\dontrun{ 162 | session <- SeleniumSession$new() 163 | 164 | # Let's create our own Shadow Root using JavaScript 165 | session$execute_script(" 166 | const div = document.createElement('div'); 167 | document.body.appendChild(div); 168 | div.attachShadow({mode: 'open'}); 169 | ") 170 | 171 | element <- session$find_element(using = "css selector", value = "div") 172 | 173 | element$shadow_root() 174 | 175 | session$close() 176 | } 177 | } 178 | \if{html}{\out{
}} 179 | 180 | } 181 | 182 | } 183 | \if{html}{\out{
}} 184 | \if{html}{\out{}} 185 | \if{latex}{\out{\hypertarget{method-ShadowRoot-find_element}{}}} 186 | \subsection{Method \code{find_element()}}{ 187 | Find an element in the shadow root. 188 | \subsection{Usage}{ 189 | \if{html}{\out{
}}\preformatted{ShadowRoot$find_element( 190 | using = c("css selector", "xpath", "tag name", "link text", "partial link text"), 191 | value, 192 | request_body = NULL, 193 | timeout = 20 194 | )}\if{html}{\out{
}} 195 | } 196 | 197 | \subsection{Arguments}{ 198 | \if{html}{\out{
}} 199 | \describe{ 200 | \item{\code{using}}{The type of selector to use.} 201 | 202 | \item{\code{value}}{The value of the selector: a string.} 203 | 204 | \item{\code{request_body}}{A list of request body parameters to pass to the 205 | Selenium server, overriding the default body of the web request} 206 | 207 | \item{\code{timeout}}{How long to wait for a request to recieve a response 208 | before throwing an error.} 209 | } 210 | \if{html}{\out{
}} 211 | } 212 | \subsection{Returns}{ 213 | A \link{WebElement} object. 214 | } 215 | \subsection{Examples}{ 216 | \if{html}{\out{
}} 217 | \preformatted{\dontrun{ 218 | session <- SeleniumSession$new() 219 | 220 | # Let's create our own Shadow Root using JavaScript 221 | session$execute_script(" 222 | const div = document.createElement('div'); 223 | document.body.appendChild(div); 224 | const shadowRoot = div.attachShadow({mode: 'open'}); 225 | const span = document.createElement('span'); 226 | span.textContent = 'Hello'; 227 | shadowRoot.appendChild(span); 228 | ") 229 | 230 | element <- session$find_element(using = "css selector", value = "div") 231 | 232 | shadow_root <- element$shadow_root() 233 | 234 | shadow_root$find_element(using = "css selector", value = "span") 235 | 236 | session$close() 237 | } 238 | } 239 | \if{html}{\out{
}} 240 | 241 | } 242 | 243 | } 244 | \if{html}{\out{
}} 245 | \if{html}{\out{}} 246 | \if{latex}{\out{\hypertarget{method-ShadowRoot-find_elements}{}}} 247 | \subsection{Method \code{find_elements()}}{ 248 | Find all elements in a shadow root matching a selector. 249 | \subsection{Usage}{ 250 | \if{html}{\out{
}}\preformatted{ShadowRoot$find_elements( 251 | using = c("css selector", "xpath", "tag name", "link text", "partial link text"), 252 | value, 253 | request_body = NULL, 254 | timeout = 20 255 | )}\if{html}{\out{
}} 256 | } 257 | 258 | \subsection{Arguments}{ 259 | \if{html}{\out{
}} 260 | \describe{ 261 | \item{\code{using}}{The type of selector to use.} 262 | 263 | \item{\code{value}}{The value of the selector: a string.} 264 | 265 | \item{\code{request_body}}{A list of request body parameters to pass to the 266 | Selenium server, overriding the default body of the web request} 267 | 268 | \item{\code{timeout}}{How long to wait for a request to recieve a response 269 | before throwing an error.} 270 | } 271 | \if{html}{\out{
}} 272 | } 273 | \subsection{Returns}{ 274 | A list of \link{WebElement} objects. 275 | } 276 | \subsection{Examples}{ 277 | \if{html}{\out{
}} 278 | \preformatted{\dontrun{ 279 | session <- SeleniumSession$new() 280 | 281 | # Let's create our own Shadow Root using JavaScript 282 | session$execute_script(" 283 | const div = document.createElement('div'); 284 | document.body.appendChild(div); 285 | const shadowRoot = div.attachShadow({mode: 'open'}); 286 | const span = document.createElement('span'); 287 | span.textContent = 'Hello'; 288 | shadowRoot.appendChild(span); 289 | const p = document.createElement('p'); 290 | p.textContent = 'Me too!'; 291 | shadowRoot.appendChild(p); 292 | ") 293 | 294 | element <- session$find_element(using = "css selector", value = "div") 295 | 296 | shadow_root <- element$shadow_root() 297 | 298 | shadow_root$find_elements(using = "css selector", value = "*") 299 | 300 | session$close() 301 | } 302 | } 303 | \if{html}{\out{
}} 304 | 305 | } 306 | 307 | } 308 | \if{html}{\out{
}} 309 | \if{html}{\out{}} 310 | \if{latex}{\out{\hypertarget{method-ShadowRoot-toJSON}{}}} 311 | \subsection{Method \code{toJSON()}}{ 312 | Convert an element to JSON. This is used by 313 | \link[=SeleniumSession]{SeleniumSession$execute_script()}. 314 | \subsection{Usage}{ 315 | \if{html}{\out{
}}\preformatted{ShadowRoot$toJSON()}\if{html}{\out{
}} 316 | } 317 | 318 | \subsection{Returns}{ 319 | A list, which can then be converted to JSON using 320 | \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}. 321 | } 322 | \subsection{Examples}{ 323 | \if{html}{\out{
}} 324 | \preformatted{\dontrun{ 325 | session <- SeleniumSession$new() 326 | 327 | # Let's create our own Shadow Root using JavaScript 328 | session$execute_script(" 329 | const div = document.createElement('div'); 330 | document.body.appendChild(div); 331 | div.attachShadow({mode: 'open'}); 332 | ") 333 | 334 | element <- session$find_element(using = "css selector", value = "div") 335 | 336 | shadow_root <- element$shadow_root() 337 | 338 | result <- shadow_root$toJSON() 339 | 340 | result 341 | 342 | jsonlite::toJSON(result, auto_unbox = TRUE) 343 | 344 | session$close() 345 | } 346 | } 347 | \if{html}{\out{
}} 348 | 349 | } 350 | 351 | } 352 | \if{html}{\out{
}} 353 | \if{html}{\out{}} 354 | \if{latex}{\out{\hypertarget{method-ShadowRoot-clone}{}}} 355 | \subsection{Method \code{clone()}}{ 356 | The objects of this class are cloneable with this method. 357 | \subsection{Usage}{ 358 | \if{html}{\out{
}}\preformatted{ShadowRoot$clone(deep = FALSE)}\if{html}{\out{
}} 359 | } 360 | 361 | \subsection{Arguments}{ 362 | \if{html}{\out{
}} 363 | \describe{ 364 | \item{\code{deep}}{Whether to make a deep clone.} 365 | } 366 | \if{html}{\out{
}} 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /man/actions_mousedown.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/actions.R 3 | \name{actions_mousedown} 4 | \alias{actions_mousedown} 5 | \alias{actions_mouseup} 6 | \alias{actions_mousemove} 7 | \title{Press, release or move the mouse} 8 | \usage{ 9 | actions_mousedown( 10 | button = c("left", "right", "middle"), 11 | width = NULL, 12 | height = NULL, 13 | pressure = NULL, 14 | tangential_pressure = NULL, 15 | tilt_x = NULL, 16 | tilt_y = NULL, 17 | twist = NULL, 18 | altitude_angle = NULL, 19 | azimuth_angle = NULL 20 | ) 21 | 22 | actions_mouseup( 23 | button = c("left", "right", "middle"), 24 | width = NULL, 25 | height = NULL, 26 | pressure = NULL, 27 | tangential_pressure = NULL, 28 | tilt_x = NULL, 29 | tilt_y = NULL, 30 | twist = NULL, 31 | altitude_angle = NULL, 32 | azimuth_angle = NULL 33 | ) 34 | 35 | actions_mousemove( 36 | x, 37 | y, 38 | duration = NULL, 39 | origin = c("viewport", "pointer"), 40 | width = NULL, 41 | height = NULL, 42 | pressure = NULL, 43 | tangential_pressure = NULL, 44 | tilt_x = NULL, 45 | tilt_y = NULL, 46 | twist = NULL, 47 | altitude_angle = NULL, 48 | azimuth_angle = NULL 49 | ) 50 | } 51 | \arguments{ 52 | \item{button}{The mouse button to press.} 53 | 54 | \item{width}{The 'width' of the click, a number.} 55 | 56 | \item{height}{The 'height' of the click, a number.} 57 | 58 | \item{pressure}{The amount of pressure to apply to the click: a number 59 | between 0 and 1.} 60 | 61 | \item{tangential_pressure}{A number between 0 and 1.} 62 | 63 | \item{tilt_x}{A whole number between -90 and 90.} 64 | 65 | \item{tilt_y}{A whole number between -90 and 90.} 66 | 67 | \item{twist}{A whole number between 0 and 359.} 68 | 69 | \item{altitude_angle}{A number between 0 and \code{pi/2}.} 70 | 71 | \item{azimuth_angle}{A number between 0 and \code{2*pi}.} 72 | 73 | \item{x}{The x coordinate of the mouse movement.} 74 | 75 | \item{y}{The y coordinate of the mouse movement.} 76 | 77 | \item{duration}{The duration of the mouse movement, in seconds.} 78 | 79 | \item{origin}{The point from which \code{x} and \code{y} are measured. Can be a 80 | \code{WebElement} object, in which case \code{x} and \code{y} are measured from the 81 | center of the element.} 82 | } 83 | \value{ 84 | A \code{selenium_action} object. 85 | } 86 | \description{ 87 | Mouse actions to be passed into \code{\link[=actions_stream]{actions_stream()}}. \code{actions_mousedown()} 88 | represents pressing a button on the mouse, while \code{actions_mouseup()} 89 | represents releasing a button. \code{actions_mousemove()} represents moving the 90 | mouse. 91 | } 92 | \examples{ 93 | actions_stream( 94 | actions_mousedown("left", width = 1, height = 1, pressure = 0.5), 95 | actions_mouseup("left", width = 100, height = 50, pressure = 1), 96 | actions_mousemove(x = 1, y = 1, duration = 1, origin = "pointer") 97 | ) 98 | 99 | } 100 | -------------------------------------------------------------------------------- /man/actions_pause.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/actions.R 3 | \name{actions_pause} 4 | \alias{actions_pause} 5 | \title{Wait for a period of time} 6 | \usage{ 7 | actions_pause(seconds) 8 | } 9 | \arguments{ 10 | \item{seconds}{The number of seconds to wait for.} 11 | } 12 | \value{ 13 | A \code{selenium_action} object. 14 | } 15 | \description{ 16 | A pause action to be passed into \code{\link[=actions_stream]{actions_stream()}}. Waits for a given 17 | number of seconds before performing the next action in the stream. 18 | } 19 | \examples{ 20 | actions_stream( 21 | actions_pause(1) 22 | ) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /man/actions_press.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/actions.R 3 | \name{actions_press} 4 | \alias{actions_press} 5 | \alias{actions_release} 6 | \title{Press or release a key} 7 | \usage{ 8 | actions_press(key) 9 | 10 | actions_release(key) 11 | } 12 | \arguments{ 13 | \item{key}{The key to press: a string consisting of a single character. Use 14 | the \link{keys} object to use special keys (e.g. \code{Ctrl}).} 15 | } 16 | \value{ 17 | A \code{selenium_action} object. 18 | } 19 | \description{ 20 | Key actions to be passed into \code{\link[=actions_stream]{actions_stream()}}. \code{actions_press()} 21 | represents pressing a key on the keyboard, while \code{actions_release()} 22 | represents releasing a key. 23 | } 24 | \examples{ 25 | actions_stream( 26 | actions_press("a"), 27 | actions_release("a"), 28 | actions_press(keys$enter), 29 | actions_release(keys$enter) 30 | ) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /man/actions_scroll.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/actions.R 3 | \name{actions_scroll} 4 | \alias{actions_scroll} 5 | \title{Scroll the page} 6 | \usage{ 7 | actions_scroll(x, y, delta_x, delta_y, duration = NULL, origin = "viewport") 8 | } 9 | \arguments{ 10 | \item{x}{The x coordinate from which the scroll action originates from.} 11 | 12 | \item{y}{The y coordinate from which the scroll action originates from.} 13 | 14 | \item{delta_x}{The number of pixels to scroll in the x direction.} 15 | 16 | \item{delta_y}{The number of pixels to scroll in the y direction.} 17 | 18 | \item{duration}{The duration of the scroll, in seconds.} 19 | 20 | \item{origin}{The point from which \code{x} and \code{y} are measured. Can be a 21 | \code{WebElement} object, in which case \code{x} and \code{y} are measured from the 22 | center of the element. Otherwise, \code{origin} must be \code{"viewport"}.} 23 | } 24 | \value{ 25 | A \code{selenium_action} object. 26 | } 27 | \description{ 28 | Scroll actions to be passed into \code{\link[=actions_stream]{actions_stream()}}. Scroll the page in 29 | a given direction. 30 | } 31 | \examples{ 32 | actions_stream( 33 | actions_scroll(x = 1, y = 1, delta_x = 1, delta_y = 1, duration = 0.5) 34 | ) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /man/actions_stream.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/actions.R 3 | \name{actions_stream} 4 | \alias{actions_stream} 5 | \title{Create a set of actions to be performed} 6 | \usage{ 7 | actions_stream(...) 8 | } 9 | \arguments{ 10 | \item{...}{\code{selenium_action} objects: the actions to perform.} 11 | } 12 | \value{ 13 | A \code{selenium_actions_stream} object, ready to be passed into 14 | \code{SeleniumSession$perform_actions()}. 15 | } 16 | \description{ 17 | \code{actions_stream()} creates a set of actions to be performed by 18 | \code{SeleniumSession$perform_actions()}. Actions are a low level way to interact 19 | with a page. 20 | } 21 | \examples{ 22 | actions_stream( 23 | actions_press(keys$enter), 24 | actions_pause(0.5), 25 | actions_release(keys$enter), 26 | actions_scroll(x = 1, y = 1, delta_x = 1, delta_y = 1, duration = 0.5), 27 | actions_mousemove(x = 1, y = 1, duration = 1, origin = "pointer") 28 | ) 29 | 30 | } 31 | \seealso{ 32 | \itemize{ 33 | \item Pause actions: \code{\link[=actions_pause]{actions_pause()}}. 34 | \item Press actions: \code{\link[=actions_press]{actions_press()}} and \code{\link[=actions_release]{actions_release()}}. 35 | \item Mouse actions: \code{\link[=actions_mousedown]{actions_mousedown()}}, \code{\link[=actions_mouseup]{actions_mouseup()}} 36 | and \code{\link[=actions_mousemove]{actions_mousemove()}}. 37 | \item Scroll actions: \code{\link[=actions_scroll]{actions_scroll()}}. 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /man/chrome_options.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/capabilities.R 3 | \name{chrome_options} 4 | \alias{chrome_options} 5 | \alias{firefox_options} 6 | \alias{edge_options} 7 | \title{Custom browser options} 8 | \usage{ 9 | chrome_options( 10 | binary = NULL, 11 | args = NULL, 12 | extensions = NULL, 13 | prefs = NULL, 14 | ... 15 | ) 16 | 17 | firefox_options(binary = NULL, args = NULL, profile = NULL, prefs = NULL, ...) 18 | 19 | edge_options(binary = NULL, args = NULL, extensions = NULL, prefs = NULL, ...) 20 | } 21 | \arguments{ 22 | \item{binary}{Path to the browser binary.} 23 | 24 | \item{args}{A character vector of additional arguments to pass to the 25 | browser.} 26 | 27 | \item{extensions}{A character vector of paths to browser extension (\code{.crx}) 28 | files. These will be base64 encoded before being passed to the browser. If 29 | you have already encoded the extensions, you can pass them using \code{\link[=I]{I()}}. 30 | For Firefox, use a profile to load extensions.} 31 | 32 | \item{prefs}{A named list of preferences to set in the browser.} 33 | 34 | \item{...}{Additional options to pass to the browser.} 35 | 36 | \item{profile}{Path to a Firefox profile directory. This will be base64 37 | encoded before being passed to the browser.} 38 | } 39 | \value{ 40 | A list of browser options, with Chrome options under the name 41 | \code{goog:chromeOptions}, Firefox options under \code{moz:firefoxOptions}, and Edge 42 | options under \code{ms:edgeOptions}. 43 | } 44 | \description{ 45 | Create browser options to pass into the \code{capabilities} argument of 46 | \link[=SeleniumSession]{SeleniumSession$new()}. 47 | } 48 | \details{ 49 | These functions allow you to more easily translate between Selenium code in 50 | other languages (e.g. Java/Python) to R. For example, consider the following 51 | Java code, adapted from the the 52 | \href{https://www.selenium.dev/documentation/webdriver/browsers/chrome/}{Selenium documentation} 53 | 54 | \if{html}{\out{
}}\preformatted{ChromeOptions options = new ChromeOptions(); 55 | 56 | options.setBinary("/path/to/chrome"); 57 | options.addArguments("--headless", "--disable-gpu"); 58 | options.addExtensions("/path/to/extension.crx"); 59 | options.setExperimentalOption("excludeSwitches", List.of("disable-popup-blocking")); 60 | }\if{html}{\out{
}} 61 | 62 | This can be translated to R as follows: 63 | 64 | \if{html}{\out{
}}\preformatted{chrome_options( 65 | binary = "/path/to/chrome", 66 | args = c("--headless", "--disable-gpu"), 67 | extensions = "/path/to/extension.crx", 68 | excludeSwitches = list("disable-popup-blocking") 69 | ) 70 | }\if{html}{\out{
}} 71 | 72 | You can combine these options with non-browser specific options simply using 73 | \code{\link[=c]{c()}}. 74 | 75 | Note that Microsoft Edge options are very similar to Chrome options, since 76 | it is based on Chromium. 77 | } 78 | \examples{ 79 | # Basic options objects 80 | chrome_options( 81 | binary = "/path/to/chrome", 82 | args = c("--headless", "--disable-gpu"), 83 | detatch = TRUE, # An additional option described in the link above. 84 | prefs = list( 85 | "profile.default_content_setting_values.notifications" = 2 86 | ) 87 | ) 88 | 89 | 90 | firefox_options(binary = "/path/to/firefox") 91 | 92 | edge_options(binary = "/path/to/edge") 93 | 94 | # Setting the user agent 95 | chrome_options(args = c("--user-agent=My User Agent")) 96 | 97 | edge_options(args = c("--user-agent=My User Agent")) 98 | 99 | firefox_options(prefs = list( 100 | "general.useragent.override" = "My User Agent" 101 | )) 102 | 103 | # Using a proxy server 104 | 105 | chrome_options(args = c("--proxy-server=HOST:PORT")) 106 | 107 | edge_options(args = c("--proxy-server=HOST:PORT")) 108 | 109 | PORT <- 1 110 | firefox_options(prefs = list( 111 | "network.proxy.type" = 1, 112 | "network.proxy.socks" = "HOST", 113 | "network.proxy.socks_port" = PORT, 114 | "network.proxy.socks_remote_dns" = FALSE 115 | )) 116 | 117 | # Combining with other options 118 | browser_options <- chrome_options(binary = "/path/to/chrome") 119 | 120 | c(browser_options, list(platformName = "Windows")) 121 | 122 | } 123 | \seealso{ 124 | For more information and examples on Chrome options, see: 125 | \url{https://developer.chrome.com/docs/chromedriver/capabilities} 126 | 127 | For Firefox options: 128 | \url{https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions} 129 | 130 | For other options that affect Firefox but are not under \code{mox:firefoxOptions}, 131 | see: 132 | \url{https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html} 133 | 134 | For Edge options, see: 135 | \url{https://learn.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options#edgeoptions-object} 136 | } 137 | -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: archived 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | archived 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: defunct 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | defunct 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: experimental 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | experimental 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: maturing 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | maturing 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: questioning 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | questioning 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-soft-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: soft-deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | soft-deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: stable 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | lifecycle 21 | 22 | 25 | 26 | stable 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: superseded 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | superseded 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/key_chord.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/keys.R 3 | \name{key_chord} 4 | \alias{key_chord} 5 | \title{Combine special keys} 6 | \usage{ 7 | key_chord(...) 8 | } 9 | \arguments{ 10 | \item{...}{The keys to be combined (strings).} 11 | } 12 | \value{ 13 | A string. 14 | } 15 | \description{ 16 | When a chord of keys is passed into \code{WebElement$send_keys()}, all keys will 17 | be pressed in order, and then released at the end. This is simply done by 18 | combining the keys into a single string, and appending the NULL key 19 | (\link[=keys]{keys$null}) to the end. This is useful for keybindings like 20 | \code{Ctrl-V}, where you want the Ctrl key to be released after the action. 21 | } 22 | \examples{ 23 | # `Ctrl-V` will be pressed, then `Ctrl-Alt-V` 24 | paste0( 25 | keys$control, "v", 26 | keys$alt, "v" 27 | ) 28 | 29 | # `Ctrl-V` will be pressed, then `Alt-V` 30 | paste0( 31 | key_chord(keys$control, "v"), 32 | key_chord(keys$alt, "v") 33 | ) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /man/keys.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/keys.R 3 | \docType{data} 4 | \name{keys} 5 | \alias{keys} 6 | \title{A list of special keys} 7 | \format{ 8 | An object of class \code{list} of length 65. 9 | } 10 | \usage{ 11 | keys 12 | } 13 | \description{ 14 | A named list of special keys, where each key is a single Unicode character, 15 | which will be interpreted by selenium as a special key. Each key is just a 16 | string, so can be used with string manipulation functions like \code{\link[=paste]{paste()}} 17 | without any special treatment. 18 | } 19 | \examples{ 20 | keys$enter 21 | 22 | } 23 | \keyword{datasets} 24 | -------------------------------------------------------------------------------- /man/selenium-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/selenium-package.R 3 | \docType{package} 4 | \name{selenium-package} 5 | \alias{selenium} 6 | \alias{selenium-package} 7 | \title{selenium: Low-Level Browser Automation Interface} 8 | \description{ 9 | An implementation of 'W3C WebDriver 2.0' (\url{https://w3c.github.io/webdriver/}), allowing interaction with a 'Selenium Server' (\url{https://www.selenium.dev/documentation/grid/}) instance from 'R'. Allows a web browser to be automated from 'R'. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://ashbythorpe.github.io/selenium-r/} 15 | \item \url{https://github.com/ashbythorpe/selenium-r} 16 | \item Report bugs at \url{https://github.com/ashbythorpe/selenium-r/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Ashby Thorpe \email{ashbythorpe@gmail.com} (\href{https://orcid.org/0000-0003-3106-099X}{ORCID}) [copyright holder] 22 | 23 | } 24 | \keyword{internal} 25 | -------------------------------------------------------------------------------- /man/selenium_server.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/server.R 3 | \name{selenium_server} 4 | \alias{selenium_server} 5 | \title{Download and start the Selenium server.} 6 | \usage{ 7 | selenium_server( 8 | version = "latest", 9 | host = "localhost", 10 | port = 4444L, 11 | selenium_manager = TRUE, 12 | interactive = TRUE, 13 | stdout = NULL, 14 | stderr = NULL, 15 | verbose = TRUE, 16 | temp = TRUE, 17 | path = NULL, 18 | extra_args = c(), 19 | ... 20 | ) 21 | } 22 | \arguments{ 23 | \item{version}{The version of Selenium Server to download and run. By 24 | default, the latest major or minor release is used.} 25 | 26 | \item{host}{The host to run the server on.} 27 | 28 | \item{port}{The port to run the server on.} 29 | 30 | \item{selenium_manager}{Whether to enable Selenium Manager, which will 31 | automatically download any missing drivers. Defaults to \code{TRUE}.} 32 | 33 | \item{interactive}{By default, if you don't have a version downloaded, you 34 | will be prompted to confirm that you want to download it, and the function 35 | will error if \code{\link[rlang:is_interactive]{rlang::is_interactive()}} returns \code{FALSE}. To allow this 36 | function to work in a non-interactive setting, set this to \code{FALSE}.} 37 | 38 | \item{stdout, stderr}{Passed into 39 | \link[processx:process]{processx::process$new()}. Set to \code{"|"} to capture 40 | output and error logs from the server, which can then be accessed with 41 | \code{server$read_output()} and \code{server$read_error()}.} 42 | 43 | \item{verbose}{Passed into \code{\link[utils:download.file]{utils::download.file()}}. Note that setting this 44 | to \code{FALSE} will \emph{not} disable the prompt if a file needs to be downloaded.} 45 | 46 | \item{temp}{Whether to use a temporary directory to download the Selenium 47 | Server \code{.jar} file. This will ensure that the file is deleted after it is 48 | used, but means that you will have to redownload the file with every new 49 | R session. If \code{FALSE}, the file is saved in your user data directory.} 50 | 51 | \item{path}{The path where the downloaded Selenium Server \code{.jar} file will 52 | be saved. Overrides \code{temp}.} 53 | 54 | \item{extra_args}{A character vector of extra arguments to pass into the 55 | Selenium Server call. See the list of options here: 56 | \url{https://www.selenium.dev/documentation/grid/configuration/cli_options/}} 57 | 58 | \item{...}{Passed into \link[processx:process]{processx::process$new()}.} 59 | } 60 | \value{ 61 | A \link{SeleniumServer} object. Call \code{server$kill()} to stop the 62 | server. 63 | } 64 | \description{ 65 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} 66 | 67 | Downloads the latest release of Selenium Server, and then runs it as a 68 | background process. You must have Java installed for this command to work. 69 | } 70 | \details{ 71 | This command respects the \code{JAVA_HOME} environment variable when attempting 72 | to find the \code{java} executable. Otherwise, \code{\link[=Sys.which]{Sys.which()}} is used. 73 | } 74 | \examples{ 75 | \dontrun{ 76 | # Disables the prompt that asks you whether you want to download Selenium server 77 | server <- selenium_server(interactive = FALSE) 78 | 79 | # Saves the server in your user data directory 80 | server <- selenium_server(temp = FALSE) 81 | server$kill() 82 | 83 | # The server doesn't have to be downloaded again 84 | server <- selenium_server(temp = FALSE) 85 | 86 | # Here we use extra arguments to increase the timeout of client sessions, 87 | # allowing sessions to stay open for longer without being automatically 88 | # terminated. 89 | server <- selenium_server(extra_args = c("--session-timeout", "3000")) 90 | } 91 | 92 | } 93 | \seealso{ 94 | The \href{https://ashbythorpe.github.io/selenium-r/index.html}{package website} 95 | for more ways to start the Selenium server. 96 | } 97 | -------------------------------------------------------------------------------- /man/wait_for_server.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/status.R 3 | \name{wait_for_server} 4 | \alias{wait_for_server} 5 | \alias{selenium_server_available} 6 | \alias{wait_for_selenium_available} 7 | \alias{get_server_status} 8 | \title{Is a selenium server instance running?} 9 | \usage{ 10 | wait_for_server( 11 | server, 12 | max_time = 60, 13 | error = TRUE, 14 | verbose = FALSE, 15 | timeout = 20 16 | ) 17 | 18 | selenium_server_available( 19 | port = 4444L, 20 | host = "localhost", 21 | verbose = FALSE, 22 | timeout = 20 23 | ) 24 | 25 | wait_for_selenium_available( 26 | max_time = 60, 27 | host = "localhost", 28 | port = 4444L, 29 | error = TRUE, 30 | verbose = FALSE, 31 | timeout = 20 32 | ) 33 | 34 | get_server_status( 35 | host = "localhost", 36 | port = 4444L, 37 | verbose = FALSE, 38 | timeout = 20 39 | ) 40 | } 41 | \arguments{ 42 | \item{server}{The process object returned by \code{\link[=selenium_server]{selenium_server()}}.} 43 | 44 | \item{max_time}{The amount of time to wait for the Selenium server to 45 | become available.} 46 | 47 | \item{error}{Whether to throw an error if the web request fails 48 | after the timeout is exceeded. If not, and we can't connect to a server, 49 | \code{FALSE} is returned.} 50 | 51 | \item{verbose}{Whether to print information about the web request that is 52 | sent.} 53 | 54 | \item{timeout}{How long to wait for a request to recieve a response before 55 | throwing an error.} 56 | 57 | \item{port}{The port that the Selenium server is using.} 58 | 59 | \item{host}{The host that the Selenium server is running on. This is 60 | usually 'localhost' (i.e. Your own machine).} 61 | } 62 | \value{ 63 | \code{wait_for_server()} and \code{wait_for_selenium_available()} return \code{TRUE} if 64 | the server is ready to be connected to, and throw an error otherwise. 65 | 66 | \code{selenium_server_available()} returns \code{TRUE} if a Selenium server is 67 | running, and \code{FALSE} otherwise. 68 | 69 | \code{get_server_status()} returns a list that can (but may not always) contain 70 | the following fields: 71 | \itemize{ 72 | \item \code{ready}: Whether the server is ready to be connected to. This should 73 | always be returned by the server. 74 | \item \code{message}: A message about the status of the server. 75 | \item \code{uptime}: How long the server has been running. 76 | \item \code{nodes}: Information about the slots that the server can take. 77 | } 78 | } 79 | \description{ 80 | \code{wait_for_server()} takes a server process returned by \code{\link[=selenium_server]{selenium_server()}} 81 | and waits for it to respond to status requests. If it doesn't, then an 82 | error is thrown detailing any errors in the response and any error messages 83 | from the server. 84 | 85 | \code{selenium_server_available()} returns \code{TRUE} if a Selenium server is 86 | running on a given port and host. \code{wait_for_selenium_available()} waits 87 | for the Selenium server to become available for a given time, throwing an 88 | error if one does not. It is similar to \code{wait_for_server()} except that it 89 | works with servers not created by selenium. 90 | 91 | \code{get_server_status()}, when given a port and host, figures out whether a 92 | Selenium server instance is running, and if so, returns its status. This is 93 | used by \code{selenium_server_available()} to figure out if the server is 94 | running. 95 | } 96 | \examples{ 97 | \dontrun{ 98 | server <- selenium_server() 99 | 100 | wait_for_server(server) 101 | 102 | get_server_status() 103 | 104 | selenium_server_available() 105 | 106 | wait_for_selenium_available() 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks 2 | library 3 | checks.noindex 4 | library.noindex 5 | cloud.noindex 6 | data.sqlite 7 | *.html 8 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Platform 2 | 3 | |field |value | 4 | |:--------|:------------------------------| 5 | |version |R version 4.4.1 (2024-06-14) | 6 | |os |Debian GNU/Linux 12 (bookworm) | 7 | |system |x86_64, linux-gnu | 8 | |ui |X11 | 9 | |language |en_GB:en | 10 | |collate |en_GB.UTF-8 | 11 | |ctype |en_GB.UTF-8 | 12 | |tz |Europe/London | 13 | |date |2024-09-23 | 14 | |pandoc |2.17.1.1 @ /usr/bin/pandoc | 15 | 16 | # Dependencies 17 | 18 | |package |old |new |Δ | 19 | |:---------|:-----|:----------|:--| 20 | |selenium |0.1.3 |0.1.3.9000 |* | 21 | |askpass |1.2.0 |1.2.0 | | 22 | |base64enc |NA |0.1-3 |* | 23 | |cli |3.6.3 |3.6.3 | | 24 | |curl |5.2.3 |5.2.3 | | 25 | |glue |1.7.0 |1.7.0 | | 26 | |httr2 |1.0.4 |1.0.4 | | 27 | |jsonlite |1.8.9 |1.8.9 | | 28 | |lifecycle |1.0.4 |1.0.4 | | 29 | |magrittr |2.0.3 |2.0.3 | | 30 | |openssl |2.2.2 |2.2.2 | | 31 | |processx |3.8.4 |3.8.4 | | 32 | |ps |1.8.0 |1.8.0 | | 33 | |R6 |2.5.1 |2.5.1 | | 34 | |rappdirs |0.3.3 |0.3.3 | | 35 | |rlang |1.1.4 |1.1.4 | | 36 | |sys |3.4.2 |3.4.2 | | 37 | |vctrs |0.6.5 |0.6.5 | | 38 | |withr |3.0.1 |3.0.1 | | 39 | 40 | # Revdeps 41 | 42 | ## Failed to check (1) 43 | 44 | |package |version |error |warning |note | 45 | |:---------|:-------|:-----|:-------|:----| 46 | |selenider |? | | | | 47 | 48 | -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 1 reverse dependencies (0 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | -------------------------------------------------------------------------------- /revdep/failures.md: -------------------------------------------------------------------------------- 1 | # selenider 2 | 3 |
4 | 5 | * Version: 6 | * GitHub: https://github.com/ashbythorpe/selenium-r 7 | * Source code: NA 8 | * Number of recursive dependencies: 0 9 | 10 |
11 | 12 | ## Error before installation 13 | 14 | ### Devel 15 | 16 | ``` 17 | 18 | 19 | 20 | Error in download.file(url, destfile, method, mode = "wb", ...) : 21 | cannot open URL 'https://cloud.r-project.org/src/contrib/httr_1.4.7.tar.gz' 22 | In addition: Warning messages: 23 | 1: In download.file(url, destfile, method, mode = "wb", ...) : 24 | URL 'https://cloud.r-project.org/src/contrib/httr_1.4.7.tar.gz': Timeout of 60 seconds was reached 25 | 2: In download.file(url, destfile, method, mode = "wb", ...) : 26 | URL 'https://cloud.r-project.org/src/contrib/httr_1.4.7.tar.gz': Timeout of 60 seconds was reached 27 | Warning in download.packages(pkgs, destdir = tmpd, available = available, : 28 | download of package ‘httr’ failed 29 | Warning in download.packages(pkgs, destdir = tmpd, available = available, : 30 | download of package ‘httr’ failed 31 | 32 | 33 | ``` 34 | ### CRAN 35 | 36 | ``` 37 | 38 | 39 | 40 | Error in download.file(url, destfile, method, mode = "wb", ...) : 41 | cannot open URL 'https://cloud.r-project.org/src/contrib/httr_1.4.7.tar.gz' 42 | In addition: Warning messages: 43 | 1: In download.file(url, destfile, method, mode = "wb", ...) : 44 | URL 'https://cloud.r-project.org/src/contrib/httr_1.4.7.tar.gz': Timeout of 60 seconds was reached 45 | 2: In download.file(url, destfile, method, mode = "wb", ...) : 46 | URL 'https://cloud.r-project.org/src/contrib/httr_1.4.7.tar.gz': Timeout of 60 seconds was reached 47 | Warning in download.packages(pkgs, destdir = tmpd, available = available, : 48 | download of package ‘httr’ failed 49 | Warning in download.packages(pkgs, destdir = tmpd, available = available, : 50 | download of package ‘httr’ failed 51 | 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(selenium) 11 | 12 | test_check("selenium") 13 | -------------------------------------------------------------------------------- /tests/testthat/helper-session.R: -------------------------------------------------------------------------------- 1 | test_session <- function(verbose = FALSE) { 2 | skip_if_offline() 3 | skip_if_not(selenium_server_available()) 4 | 5 | browser <- Sys.getenv("SELENIUM_BROWSER", "firefox") 6 | port <- as.integer(Sys.getenv("SELENIUM_PORT", 4444L)) 7 | host <- Sys.getenv("SELENIUM_HOST", "localhost") 8 | 9 | session <- try(SeleniumSession$new( 10 | browser = browser, 11 | port = port, 12 | host = host, 13 | verbose = verbose, 14 | )) 15 | 16 | if (inherits(session, "try-error")) { 17 | skip("Selenium session failed to start: You must have a Selenium server running.") 18 | } 19 | 20 | session 21 | } 22 | 23 | test_helper_site <- function(verbose = FALSE) { 24 | file <- normalizePath(testthat::test_path("helper-site.html")) 25 | 26 | if (grepl("/tmp/", file) || is_check()) { 27 | skip("Browsers cannot access HTML files in the temporary directory.") 28 | } 29 | 30 | session <- test_session(verbose = verbose) 31 | 32 | session$navigate(paste0("file://", file)) 33 | session 34 | } 35 | 36 | is_cran_check <- function() { 37 | if (env_var_is_true("NOT_CRAN")) { 38 | FALSE 39 | } else { 40 | is_check() 41 | } 42 | } 43 | 44 | is_check <- function() Sys.getenv("_R_CHECK_PACKAGE_NAME_", "") != "" 45 | -------------------------------------------------------------------------------- /tests/testthat/helper-site.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Website 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |

Example text

26 |
Fixed size div
27 |
28 | 29 |

To be hidden

30 | 31 |
32 |
33 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/testthat/test-element.R: -------------------------------------------------------------------------------- 1 | test_that("Finding WebElements works", { 2 | session <- test_session() 3 | 4 | element <- session$find_element(value = "*") 5 | 6 | expect_s3_class(element, "WebElement") 7 | 8 | element_2 <- element$find_element(value = "*") 9 | 10 | expect_s3_class(element_2, "WebElement") 11 | 12 | elements <- element$find_elements(value = "*") 13 | 14 | expect_equal(element_2$id, elements[[1]]$id) 15 | 16 | session$close() 17 | }) 18 | 19 | test_that("Predicates on WebElements work", { 20 | session <- test_helper_site() 21 | 22 | element <- session$find_element(value = "#radio-buttons")$find_element(value = "#radio-1") 23 | 24 | expect_equal(element$is_selected(), FALSE) 25 | element$click() 26 | expect_equal(element$is_selected(), TRUE) 27 | 28 | buttons <- session$find_element(value = "#buttons")$find_elements(value = "button") 29 | expect_equal(buttons[[1]]$is_enabled(), TRUE) 30 | expect_equal(buttons[[2]]$is_enabled(), FALSE) 31 | 32 | expect_equal(buttons[[1]]$is_displayed(), TRUE) 33 | 34 | session$close() 35 | }) 36 | 37 | test_that("Properties work", { 38 | session <- test_helper_site() 39 | 40 | div <- session$find_element(value = "#properties") 41 | 42 | button <- div$find_element(value = "button") 43 | expect_equal(button$get_attribute("id"), "property-1") 44 | input <- div$find_element(value = "input") 45 | input$send_keys(" A") 46 | expect_equal(input$get_property("value"), "Initial value A") 47 | expect_equal(input$get_attribute("value"), "Initial value") 48 | 49 | hidden_div <- div$find_element(value = ".hidden-div") 50 | expect_equal(hidden_div$get_css_value("display"), "none") 51 | 52 | p <- div$find_element(value = ".text") 53 | expect_equal(p$get_text(), "Example text") 54 | expect_equal(div$get_tag_name(), "div") 55 | expect_equal(p$get_tag_name(), "p") 56 | 57 | fixed_size_div <- div$find_element(value = ".fixed-size") 58 | expect_equal(fixed_size_div$get_rect()[c("width", "height")], list(width = 100, height = 100)) 59 | 60 | radio_button <- session$find_element(value = "#radio-buttons")$find_element(value = "#radio-1") 61 | 62 | expect_equal(fixed_size_div$computed_role(), "generic") 63 | expect_equal(radio_button$computed_label(), "Option 1") 64 | 65 | expect_no_error(radio_button$screenshot()) 66 | 67 | session$close() 68 | }) 69 | 70 | test_that("Actions work", { 71 | session <- test_helper_site() 72 | 73 | div <- session$find_element(value = "#interactible") 74 | 75 | p <- div$find_element(value = "p") 76 | 77 | expect_true(p$get_css_value("display") != "none") 78 | 79 | button <- div$find_element(value = "button") 80 | button$click() 81 | Sys.sleep(0.1) 82 | expect_equal(p$get_css_value("display"), "none") 83 | 84 | input <- div$find_element(value = "input") 85 | 86 | expect_equal(input$get_property("value"), "Initial value") 87 | input$clear() 88 | expect_equal(input$get_property("value"), "") 89 | input$send_keys("A") 90 | expect_equal(input$get_property("value"), "A") 91 | 92 | session$close() 93 | }) 94 | 95 | test_that("ShadowRoot elements work", { 96 | session <- test_helper_site() 97 | 98 | div <- session$find_element(value = "#shadow-container") 99 | 100 | shadow_root <- div$shadow_root() 101 | 102 | expect_s3_class(shadow_root, "ShadowRoot") 103 | 104 | expect_length(shadow_root$find_elements(value = "*"), 2) 105 | expect_equal(shadow_root$find_element(value = "p")$get_text(), "Me too!") 106 | 107 | res <- session$execute_script(" 108 | const root = arguments[0]; 109 | const div = document.createElement('div'); 110 | div.innerText = 'Me three!'; 111 | root.appendChild(div); 112 | return root; 113 | ", shadow_root) 114 | 115 | expect_equal(res$id, shadow_root$id) 116 | 117 | expect_length(shadow_root$find_elements(value = "*"), 3) 118 | 119 | session$close() 120 | }) 121 | -------------------------------------------------------------------------------- /tests/testthat/test-keys.R: -------------------------------------------------------------------------------- 1 | test_that("key_chord() works", { 2 | expect_equal( 3 | key_chord(keys$enter, keys$shift, "A"), 4 | paste0(keys$enter, keys$shift, "A", keys$null) 5 | ) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/testthat/test-server.R: -------------------------------------------------------------------------------- 1 | test_that("selenium_server() works", { 2 | skip_if_offline() 3 | skip_if(is_check()) 4 | skip_on_ci() 5 | 6 | dir <- withr::local_tempdir() 7 | withr::local_envvar(list(R_USER_DATA_DIR = dir)) 8 | 9 | expect_no_error({ 10 | session <- selenium_server(interactive = FALSE) 11 | 12 | session$kill() 13 | 14 | session2 <- selenium_server(interactive = FALSE, version = "4.10.0") 15 | 16 | session2$kill() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/testthat/test-session.R: -------------------------------------------------------------------------------- 1 | test_that("Opening and closing a SeleniumSession works", { 2 | session <- test_session() 3 | 4 | expect_type(session$id, "character") 5 | 6 | session$close() 7 | }) 8 | 9 | test_that("Server status works", { 10 | session <- test_session() 11 | 12 | expect_no_error(session$status()$ready) 13 | 14 | session$close() 15 | 16 | expect_no_error(session$status()$ready) 17 | }) 18 | 19 | test_that("Getting and setting timeouts works", { 20 | session <- test_session() 21 | 22 | expect_type(session$get_timeouts(), "list") 23 | expect_length(session$get_timeouts(), 3) 24 | 25 | session$set_timeouts(script = 100) 26 | expect_equal(session$get_timeouts()$script, 100) 27 | 28 | session$set_timeouts(page_load = 200, implicit_wait = 300) 29 | timeouts <- session$get_timeouts() 30 | expect_equal(timeouts$script, 100) 31 | expect_equal(timeouts$pageLoad, 200) 32 | expect_equal(timeouts$implicit, 300) 33 | 34 | session$close() 35 | }) 36 | 37 | test_that("Navigating works", { 38 | session <- test_session() 39 | 40 | session$navigate("https://www.google.com/") 41 | 42 | expect_equal(session$current_url(), "https://www.google.com/") 43 | 44 | session$navigate("https://www.r-project.org/") 45 | 46 | expect_equal(session$current_url(), "https://www.r-project.org/") 47 | 48 | session$back() 49 | 50 | expect_equal(session$current_url(), "https://www.google.com/") 51 | 52 | session$forward() 53 | 54 | expect_equal(session$current_url(), "https://www.r-project.org/") 55 | 56 | session$refresh() 57 | 58 | expect_equal(session$current_url(), "https://www.r-project.org/") 59 | 60 | session$navigate("https://www.tidyverse.org/") 61 | 62 | expect_equal(session$title(), "Tidyverse") 63 | 64 | session$close() 65 | }) 66 | 67 | test_that("Windows work", { 68 | session <- test_session() 69 | 70 | session$navigate("https://www.r-project.org/") 71 | 72 | result <- session$new_window() 73 | 74 | expect_equal(result$type, "tab") 75 | expect_type(result$handle, "character") 76 | 77 | expect_length(session$window_handles(), 2) 78 | expect_equal(session$window_handles()[[2]], result$handle) 79 | 80 | expect_equal(session$current_url(), "https://www.r-project.org/") 81 | 82 | session$switch_to_window(result$handle) 83 | 84 | handles <- session$close_window() 85 | expect_equal(session$window_handles(), handles) 86 | session$switch_to_window(handles[[1]]) 87 | 88 | expect_equal(session$current_url(), "https://www.r-project.org/") 89 | 90 | result <- session$new_window(type = "window") 91 | 92 | expect_equal(result$type, "window") 93 | expect_type(result$handle, "character") 94 | 95 | session$switch_to_window(result$handle) 96 | 97 | session$close() 98 | }) 99 | 100 | test_that("Switching to frames works", { 101 | session <- test_session() 102 | 103 | session$navigate("https://www.youtube.com") 104 | 105 | expect_true(length(session$find_elements(value = "iframe")) > 0) 106 | 107 | session$switch_to_frame(1) 108 | 109 | expect_error(session$switch_to_frame(0)) 110 | 111 | session$switch_to_frame() 112 | 113 | element <- session$find_element(value = "iframe") 114 | 115 | session$switch_to_frame(element) 116 | 117 | session$switch_to_parent_frame() 118 | 119 | session$close() 120 | }) 121 | 122 | test_that("Changing window size works", { 123 | session <- test_session() 124 | 125 | session$navigate("https://www.r-project.org") 126 | 127 | expect_length(session$get_window_rect(), 4) 128 | 129 | rect <- session$set_window_rect(width = 800, height = 600, x = 2, y = 3) 130 | 131 | expect_equal(session$get_window_rect(), rect) 132 | 133 | other_window <- session$new_window(type = "window")$handle 134 | 135 | session$switch_to_window(other_window) 136 | 137 | # These can fail on some browsers, but at least take a look at the output 138 | try(session$maximize_window()) 139 | try(session$minimize_window()) 140 | try(session$fullscreen_window()) 141 | 142 | session$close() 143 | }) 144 | 145 | test_that("Finding elements works", { 146 | session <- test_session() 147 | 148 | expect_s3_class(session$get_active_element(), "WebElement") 149 | 150 | expect_equal(session$find_element(value = "*")$get_tag_name(), "html") 151 | 152 | elements <- session$find_elements(value = "*") 153 | expect_length(elements, 3) 154 | 155 | for (element in elements) { 156 | expect_s3_class(element, "WebElement") 157 | } 158 | 159 | session$close() 160 | }) 161 | 162 | test_that("Get page source works", { 163 | session <- test_session() 164 | 165 | source <- session$get_page_source() 166 | 167 | expect_type(source, "character") 168 | 169 | skip_if_not_installed("xml2") 170 | 171 | expect_no_error(xml2::read_html(source)) 172 | 173 | session$navigate("https://www.r-project.org/") 174 | 175 | source <- session$get_page_source() 176 | 177 | expect_no_error(xml2::read_html(source)) 178 | 179 | session$close() 180 | }) 181 | 182 | test_that("Execute script works", { 183 | session <- test_session() 184 | 185 | element <- session$execute_script("return document.querySelector('*');") 186 | expect_s3_class(element, "WebElement") 187 | 188 | element2 <- session$find_element(value = "*") 189 | expect_equal(element$id, element2$id) 190 | 191 | element3 <- session$execute_script("return arguments[0];", element) 192 | expect_equal(element$id, element3$id) 193 | 194 | expect_equal(session$execute_script("return arguments[0] + arguments[1];", 1, 1), 2) 195 | 196 | x <- session$execute_async_script(" 197 | let callback = arguments[arguments.length - 1]; 198 | let check = function(n) { 199 | if (n == 0) { 200 | return callback(n); 201 | } else { 202 | setTimeout(function() { 203 | check(n - 1); 204 | }, 1000); 205 | } 206 | } 207 | check(2); 208 | ") 209 | 210 | expect_equal(x, 0) 211 | 212 | session$close() 213 | }) 214 | 215 | test_that("Changing cookies works", { 216 | session <- test_session() 217 | 218 | session$navigate("https://www.google.com/") 219 | 220 | cookies <- session$get_cookies() 221 | 222 | expect_true(length(cookies) > 0) 223 | 224 | cookie_1 <- cookies[[1]] 225 | 226 | expect_equal(session$get_cookie(cookie_1$name), cookie_1) 227 | 228 | my_cookie <- list(name = "my_cookie", value = "my_value") 229 | 230 | session$add_cookie(my_cookie) 231 | 232 | expect_equal(session$get_cookie("my_cookie")$value, "my_value") 233 | 234 | session$delete_cookie("my_cookie") 235 | 236 | expect_error(session$get_cookie("my_cookie")) 237 | 238 | session$delete_all_cookies() 239 | 240 | expect_true(length(session$get_cookies()) <= 1) 241 | 242 | session$close() 243 | }) 244 | 245 | test_that("Performing actions works", { 246 | session <- test_session() 247 | 248 | session$navigate("https://www.google.com/") 249 | 250 | element <- session$find_element(value = "*") 251 | 252 | actions <- actions_stream( 253 | actions_pause(1), 254 | actions_press("a"), 255 | actions_release("a"), 256 | actions_mousedown( 257 | button = "middle", width = 1, height = 1, pressure = 0.5, 258 | tangential_pressure = 1, tilt_x = 1, tilt_y = 1, 259 | twist = 2, altitude_angle = 1, azimuth_angle = 2 260 | ), 261 | actions_mouseup( 262 | button = "middle", width = 100, height = 50, pressure = 1, 263 | tangential_pressure = 0.1, tilt_x = -1, tilt_y = 8, 264 | twist = 10, altitude_angle = pi / 2 - 1, azimuth_angle = 0 265 | ), 266 | actions_mousemove( 267 | x = 1, 268 | y = 1, 269 | duration = 1, 270 | origin = "pointer", 271 | azimuth_angle = pi / 2 272 | ), 273 | actions_scroll(x = 1, y = 1, delta_x = 1, delta_y = 1, duration = 0.5), 274 | actions_mousemove(x = 0, y = 0, origin = element), 275 | actions_scroll(x = 0, y = 0, delta_x = 1, delta_y = 1, origin = element) 276 | ) 277 | 278 | expect_no_error(session$perform_actions(actions)) 279 | 280 | session$close() 281 | }) 282 | 283 | test_that("Handling alerts works", { 284 | session <- test_session() 285 | 286 | session$execute_script("alert('Hello')") 287 | expect_equal(session$get_alert_text(), "Hello") 288 | session$dismiss_alert() 289 | 290 | session$execute_script("alert('Hello')") 291 | session$accept_alert() 292 | 293 | session$execute_script("prompt('Enter text:')") 294 | session$send_alert_text("Hello") 295 | session$dismiss_alert() 296 | 297 | session$close() 298 | }) 299 | 300 | test_that("Screenshotting and printing works", { 301 | session <- test_session() 302 | 303 | expect_no_error(session$screenshot()) 304 | 305 | # Doesn't work on all browsers 306 | try(session$print_page()) 307 | 308 | session$close() 309 | }) 310 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/articles/debugging.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Debugging selenium" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | available <- selenium::selenium_server_available() 7 | knitr::opts_chunk$set( 8 | collapse = TRUE, 9 | comment = "#>", 10 | eval = available 11 | ) 12 | ``` 13 | 14 | ```{r eval = !available, echo = FALSE, comment = NA} 15 | if (!available) { 16 | message("Selenium server is not available.") 17 | } 18 | ``` 19 | 20 | Web automation is a complex and fragile task, with many ways to go wrong. 21 | This article describes some common errors and pitfalls, and how you might 22 | go about resolving them. 23 | 24 | ## Starting the client 25 | 26 | If initializing a `SeleniumSession` fails, it is often useful to look at 27 | the logs from the server. If you ran the `java -jar ...` command manually, then 28 | you should be able to see the logs, but if you used `selenium_server()`, then 29 | the logs are unavailable by default. However, you can use the `stdout` and 30 | `stderr` arguments to enable log collection, and then use `read_output()` and 31 | `read_error()` to read the logs. 32 | 33 | ```{r eval = FALSE} 34 | server <- selenium_server(stdout = "|", stderr = "|") 35 | server$read_output() 36 | server$read_error() 37 | ``` 38 | 39 | This will show any output/errors that the server has written to the console. 40 | 41 | ### Timeout 42 | 43 | Sometimes, the server can just take a very long time to start up. If you get 44 | an error from `wait_for_server()` or `wait_for_selenium_available()`, it can 45 | be worth increasing the `max_time` argument to something higher than 60, and 46 | seeing if that fixes the issue. 47 | 48 | ### Wrong Java version 49 | 50 | If starting the server results in an error, and `wait_for_server()`, 51 | `server$read_error()` or the logs show an error similar to: 52 | 53 | `... java.lang.UnsupportedClassVersionError: {file} has been compiled by a more 54 | recent version of the Java Runtime ...` 55 | 56 | This probably means that you need to update your Java version. Selenium's 57 | minimum version is now Java 11 (see 58 | ). You can find later 59 | versions of Java [here](https://www.oracle.com/java/technologies/downloads/) 60 | 61 | ### Port and IP address 62 | One reason why you may be unable to connect to the server is that the port and 63 | IP address you are connecting to is wrong. 64 | 65 | If you are using `selenium_server()`, `server$host` and `server$port` give you 66 | the host IP address and port, respectively. 67 | 68 | You can also get the IP address and port from the server logs. You should see a 69 | line like: 70 | `INFO [Standalone.execute] - Started Selenium Standalone ... (revision ...): http://:` 71 | 72 | The URL at the end of this message can be used to extract an IP address and a 73 | port number, which can then be passed into the `host` and `port` arguments. For 74 | example, if the URL was: `http://172.17.0.1/4444`, you would run: 75 | 76 | ```{r eval = FALSE} 77 | session <- SeleniumSession$new(host = "172.17.0.1", port = 4444) 78 | ``` 79 | 80 | ### Using a different debugging port for Chrome 81 | If you are using Chrome, and you see a browser open, but the call to 82 | `SeleniumSession$new()` times out, you may need to use a different debugging port. 83 | For example: 84 | 85 | ```{r eval = FALSE} 86 | session <- SeleniumSession$new( 87 | browser = "chrome", 88 | capabilities = list( 89 | `goog:chromeOptions` = list( 90 | args = list("remote-debugging-port=9222") 91 | ) 92 | ) 93 | ) 94 | ``` 95 | 96 | ### Increasing /dev/shm/ size when using docker 97 | If you are running selenium using docker, you may need to increase the size of 98 | `/dev/shm/` to avoid running out of memory. This issue usually happens when 99 | using Chrome, and usually results in a message like 100 | `session deleted because of page crash`. 101 | 102 | You can use the `--shm-size` to the selenium docker images to fix this issue. 103 | For example: 104 | `docker run --shm-size="2g" selenium/standalone-chrome:` 105 | 106 | ## Other common errors 107 | 108 | ### Stale element reference errors 109 | At some point, when using selenium, you will encounter the following error: 110 | 111 | ```{r eval = FALSE} 112 | #> Error in `element$click()`: 113 | #> ! Stale element reference. 114 | #> ✖ The element with the reference <...> is not known in the current browsing context 115 | #> Caused by error in `httr2::req_perform()`: 116 | #> ! HTTP 404 Not Found. 117 | #> Run `rlang::last_trace()` to see where the error occurred. 118 | ``` 119 | 120 | This error is common when automating a website. Selenium is telling you that 121 | an element which you previously identified no longer exists. In all websites, 122 | especially complex ones, the DOM will be constantly updating itself, constantly 123 | invalidating references to elements. This error is a particularly annoying one, 124 | as it can happen at any time and is impossible to predict. 125 | 126 | One way to deal with this error is to use elements as soon as they are created, 127 | only keeping references to elements if you are sure that they will not be 128 | invalidated. For example, if you want to click the same element twice, with 129 | a second-long gap in between, you may want to consider fetching the element 130 | once for each time, rather than sharing the reference between the actions. 131 | 132 | However, this solution is not infallible. If you find yourself encountering 133 | this error a lot, it may be a sign that a more high-level package, that can 134 | deal with this issue (e.g. [selenider](https://github.com/ashbythorpe/selenider)), 135 | is needed. 136 | -------------------------------------------------------------------------------- /vignettes/articles/selenium.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started with selenium" 3 | --- 4 | 5 | ```{r, include = FALSE} 6 | available <- selenium::selenium_server_available() 7 | knitr::opts_chunk$set( 8 | collapse = TRUE, 9 | comment = "#>", 10 | eval = available 11 | ) 12 | ``` 13 | 14 | ```{r eval = !available, echo = FALSE, comment = NA} 15 | if (!available) { 16 | message("Selenium server is not available.") 17 | } 18 | ``` 19 | 20 | selenium is low-level tool, so can be complex to use. This vignette gives an 21 | overview of the important functions of selenium, and how to use them. 22 | 23 | ```{r setup} 24 | library(selenium) 25 | ``` 26 | 27 | First, let's create a new session (assuming that you have a server running): 28 | ```{r session} 29 | session <- SeleniumSession$new() 30 | ``` 31 | 32 | First, navigate to the website you want to automate with `SeleniumSession$navigate()`: 33 | 34 | ```{r navigate} 35 | session$navigate("http://www.tidyverse.org") 36 | ``` 37 | 38 | ## Navigation 39 | 40 | Use `SeleniumSession$back()` and `SeleniumSession$forward()` to navigate 41 | back and forward in the navigation history, and 42 | `SeleniumSession$refresh()` to reload the current page 43 | 44 | ```{r history} 45 | session$back() 46 | 47 | session$forward() 48 | 49 | session$refresh() 50 | 51 | session$current_url() 52 | ``` 53 | 54 | ## Elements 55 | 56 | To interact with a webpage, you first need to find the elements you want to 57 | interact with. You can do this using `SeleniumSession$find_element()` and 58 | `SeleniumSession$find_elements()`. Both functions take two arguments: 59 | `using` and `value`. `using` specifies the method you want to use to find the 60 | element, while `value` is the string you want to search for. 61 | 62 | By default, CSS selectors are used (`value = "css selector"`), as they provide 63 | a simple and concise way to find elements: 64 | 65 | * Use `"name"` to select an element with the tag name `name`. (e.g. `div`). 66 | * Use `"#id"` to select an element with the id `id`. 67 | * Use `".class"` to select an element with the class `class`. 68 | * Use square brackets to select elements using other properties. 69 | For example, `"input[type='text']"` selects a text input element. 70 | 71 | For more information about using CSS selectors, see: 72 | . 73 | 74 | Another notable way to select elements is using XPaths. XPaths can be slightly more 75 | verbose than CSS selectors, but are more powerful. Use `using = "xpath"` to use 76 | XPaths. 77 | 78 | * Use `//tagname` to select an element with the tag name `tagname`. 79 | * Use `//tagname[@attribute='value']` to select an element with the tag name 80 | `tagname` and the attribute `attribute` with the value `value`. 81 | * Use `//tagname/childtagname` (single slash) to select an element that is a direct 82 | child of the `tagname` element. Similarly, `/tagname` can be used to select 83 | an element that is at the top level of the DOM. 84 | * Use `//tagname//childtagname` to select an element that is a child of the 85 | `tagname` element, but does not have to be directly below it in the DOM. 86 | 87 | To learn more about how to use XPaths, see: 88 | 89 | 90 | The other values that `using` can take are fairly self-explanatory (e.g. `"tag name"`) 91 | will match elements with the tag name given by `value`. 92 | 93 | Once you have an element, you can locate further sub-elements using 94 | `WebElement$find_element()` and `WebElement$find_elements()`, which will 95 | now select from the element's children. 96 | This is especially useful when a website does not make much use of 97 | classes and ids to label its elements uniquely. 98 | 99 | For example, the following code will find all the hex images on the Tidyverse 100 | home page: 101 | 102 | ```{r hex} 103 | hex_stickers <- session$ 104 | find_element(using = "css selector", value = ".hexBadges")$ 105 | find_elements(using = "css selector", value = "img") 106 | ``` 107 | 108 | ## Element properties 109 | Once you have an element, there are many properties that you can request from 110 | it. 111 | 112 | ```{r get_element} 113 | element <- hex_stickers[[1]] 114 | ``` 115 | 116 | Use `WebElement$get_tag_name()` to get the tag name of an element: 117 | 118 | ```{r tag_name} 119 | element$get_tag_name() 120 | ``` 121 | 122 | Use `WebElement$get_text()` to get the text inside of an element: 123 | 124 | ```{r text} 125 | element$get_text() 126 | ``` 127 | 128 | `WebElement$get_attribute()` and `WebElement$get_property()` will return 129 | the value or property of an element (see the documentation for the 130 | difference between the two). 131 | 132 | ```{r get_attribute} 133 | element$get_attribute("src") 134 | 135 | element$get_property("src") 136 | ``` 137 | 138 | `WebElement$get_css_value()` will return the computed CSS property of an 139 | element: 140 | 141 | ```{r get_css_value} 142 | element$get_css_value("background-color") 143 | ``` 144 | 145 | Use `WebElement$get_rect()` to get the coordinates and size of an element: 146 | 147 | ```{r rect} 148 | element$get_rect() 149 | ``` 150 | 151 | Use `WebElement$is_selected()`, `WebElement$is_enabled()` and 152 | `WebElement$is_displayed()` to check if an element is currently selected, 153 | enabled or visible. 154 | 155 | ```{r check} 156 | element$is_selected() 157 | 158 | element$is_enabled() 159 | 160 | element$is_displayed() 161 | ``` 162 | 163 | ## Interacting with an element 164 | There are many different ways to interact with an element. 165 | 166 | Use `WebElement$click()` to click on an element: 167 | 168 | ```{r click} 169 | element$click() 170 | ``` 171 | 172 | Use `WebElement$send_keys()` to send text to an input element: 173 | 174 | ```{r send_keys} 175 | input <- session$find_element(using = "css selector", value = "input[type='search']") 176 | 177 | input$send_keys("filter") 178 | ``` 179 | 180 | You can use the `keys` object to send more advanced key combinations to an element. 181 | 182 | For example, the below code selects all the text inside the element (``), and 183 | then deletes it. 184 | 185 | ```{r keys} 186 | input$send_keys(keys$control, "a", keys$backspace) 187 | ``` 188 | 189 | However, generally, if you want to clear an input element, use `WebElement$clear()`. 190 | 191 | ```{r clear} 192 | input$clear() 193 | ``` 194 | 195 | ## Custom JavaScript 196 | Use `SeleniumSession$execute_script()` to execute custom JavaScript. Any extra arguments 197 | will be passed into the script through the `arguments` array, and items can be returned 198 | explicitly using `return`. `WebElements` can also be passed in and returned. 199 | 200 | ```{r js} 201 | session$execute_script("return arguments[0] + arguments[1];", 2, 2) 202 | ``` 203 | 204 | ```{r js_element} 205 | session$execute_script("return arguments[0].parentElement;", input) 206 | ``` 207 | 208 | `SeleniumSession$execute_async_script()` is provided for when you need to interact 209 | with an asynchronous JavaScript API, but is much less commonly used. 210 | 211 | ## Windows and Tabs 212 | A `SeleniumSession` object is not limited to a single window/tab. Use 213 | `SeleniumSession$new_window()` to create a new window or tab, using the 214 | `type` argument to choose between the two: 215 | 216 | ```{r new_window} 217 | tab <- session$new_window(type = "tab") 218 | 219 | window <- session$new_window(type = "window") 220 | 221 | tab 222 | 223 | window 224 | ``` 225 | 226 | Each window/tab has its own handle: a string that uniquely identifies it. 227 | Use `SeleniumSession$window_handle()` to get the handle of the current 228 | window, and `SeleniumSession$window_handles()` to get the handle of 229 | every open window/tab. 230 | 231 | ```{r window_handle} 232 | current_handle <- session$window_handle() 233 | 234 | current_handle 235 | 236 | session$window_handles() 237 | ``` 238 | 239 | When a new window/tab is created, it is not automatically switched to. Use 240 | `SeleniumSession$switch_to_window()` to switch to a window/tab with a specific 241 | handle: 242 | 243 | ```{r switch_to_window} 244 | session$switch_to_window(window$handle) 245 | ``` 246 | 247 | Use `SeleniumSession$close_window()` to close a window/tab: 248 | 249 | ```{r close_window} 250 | session$close_window() 251 | 252 | # Switch to an open window 253 | session$switch_to_window(current_handle) 254 | ``` 255 | 256 | ## iframes and the Shadow DOM 257 | There are many methods that web developers use to embed self-contained 258 | HTML documents or elements into a page. 259 | 260 | One of these is an iframe element, allowing a nested browsing context to 261 | be embedded into a page. For example, iframes are often used to embed 262 | video players into a site. Use `SeleniumSession$switch_to_frame()` to switch 263 | to an frame and select elements within it. You can pass in the index of the 264 | frame, or the `iframe` element itself. 265 | 266 | ```{r switch_to_frame} 267 | session$navigate("https://www.youtube.com") 268 | 269 | iframe <- session$find_element(using = "css selector", value = "iframe") 270 | 271 | session$switch_to_frame(iframe) 272 | ``` 273 | 274 | Use `SeleniumSession$switch_to_parent_frame()` to switch to the parent 275 | frame of the current one, or use `SeleniumSession$switch_to_frame()` 276 | with no arguments to switch to the top-level browsing context: 277 | 278 | ```{r switch_to_parent_frame} 279 | session$switch_to_parent_frame() 280 | ``` 281 | 282 | The Shadow DOM is another way for websites to embed content into a page, 283 | this time into any element. If an element is a shadow root, use 284 | `WebElement$shadow_root()` to get the corresponding `ShadowRoot` 285 | object. You can then find sub-elements within the shadow root 286 | using `ShadowRoot$find_element()` and `ShadowRoot$find_elements()`. 287 | 288 | ## Closing the session 289 | Always remember to close the session after you have finished using it: 290 | 291 | ```{r close} 292 | session$close() 293 | ``` 294 | --------------------------------------------------------------------------------