├── .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 | [](https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml)
28 | [](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 | [](https://github.com/ashbythorpe/selenium-r/actions/workflows/R-CMD-check.yaml)
9 | [](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{
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{
}}
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{
}}
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{
}}
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{
}}
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{
}}
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{
}}
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 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-defunct.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-deprecated.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-experimental.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-maturing.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-questioning.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-soft-deprecated.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-stable.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-superseded.svg:
--------------------------------------------------------------------------------
1 |
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 |
Hidden div
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 |
--------------------------------------------------------------------------------