├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── R-CMD-check.yaml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── browser.R ├── callbacks.R ├── chrome.R ├── chromote-package.R ├── chromote.R ├── chromote_session.R ├── event_manager.R ├── import-standalone-obj-type.R ├── import-standalone-types-check.R ├── manage.R ├── promises.R ├── protocol.R ├── screenshot.R ├── synchronize.R ├── utils.R └── zzz.R ├── README.Rmd ├── README.md ├── chromote.Rproj ├── cran-comments.md ├── man ├── Browser.Rd ├── Chrome.Rd ├── ChromeRemote.Rd ├── Chromote.Rd ├── ChromoteSession.Rd ├── chrome_versions.Rd ├── chrome_versions_list.Rd ├── chromote-options.Rd ├── chromote-package.Rd ├── chromote_info.Rd ├── default_chrome_args.Rd ├── default_chromote_object.Rd ├── figures │ ├── lifecycle-experimental.svg │ ├── logo.png │ └── sidebar.png ├── find_chrome.Rd ├── fragments │ ├── basic-usage.Rmd │ ├── features.Rmd │ └── install.Rmd ├── reexports.Rd └── with_chrome_version.Rd ├── pkgdown ├── _brand.yml ├── _pkgdown.yml ├── extra.scss └── favicon │ ├── apple-touch-icon.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── site.webmanifest │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png ├── revdep ├── .gitignore ├── README.md ├── cran.md ├── failures.md └── problems.md ├── tests ├── testthat.R └── testthat │ ├── _snaps │ ├── chromote_session.md │ ├── linux64 │ │ └── manage.md │ ├── mac-arm64 │ │ └── manage.md │ └── win64 │ │ └── manage.md │ ├── helper.R │ ├── setup.R │ ├── test-chrome.R │ ├── test-chromote_session.R │ ├── test-default_chromote_args.R │ ├── test-manage.R │ └── test-utils.R └── vignettes ├── .gitignore ├── chromote.Rmd ├── commands-and-events.Rmd ├── example-attach-existing.Rmd ├── example-authentication.Rmd ├── example-cran-tests.Rmd ├── example-custom-headers.Rmd ├── example-custom-user-agent.Rmd ├── example-extract-text.Rmd ├── example-loading-page.Rmd ├── example-remote-hosts.Rmd ├── example-screenshot.Rmd ├── sync-async.Rmd └── which-chrome.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^chromote\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^temp$ 4 | ^chromote\.sublime-project$ 5 | ^\.github$ 6 | ^_pkgdown\.yml$ 7 | ^docs$ 8 | ^pkgdown$ 9 | ^README\.Rmd$ 10 | ^sidebar.png$ 11 | ^revdep$ 12 | ^cran-comments\.md$ 13 | ^CRAN-SUBMISSION$ 14 | ^_dev$ 15 | ^\.vscode$ 16 | ^[\.]?air\.toml$ 17 | ^LICENSE\.md$ 18 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/rstudio/shiny-workflows 2 | # 3 | # NOTE: This Shiny team GHA workflow is overkill for most R packages. 4 | # For most R packages it is better to use https://github.com/r-lib/actions 5 | on: 6 | push: 7 | branches: [main, rc-**] 8 | pull_request: 9 | branches: [main] 10 | schedule: 11 | - cron: '0 8 * * 1' # every monday 12 | 13 | name: Package checks 14 | 15 | jobs: 16 | website: 17 | uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1 18 | routine: 19 | uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1 20 | with: 21 | format-r-code: true 22 | R-CMD-check: 23 | uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rhistory 2 | .RData 3 | .Rproj.user 4 | temp 5 | docs 6 | CRAN-SUBMISSION 7 | inst/doc 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Posit.air-vscode" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[r]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "Posit.air-vscode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: chromote 2 | Title: Headless Chrome Web Browser Interface 3 | Version: 0.5.1.9000 4 | Authors@R: c( 5 | person("Garrick", "Aden-Buie", , "garrick@posit.co", role = c("aut", "cre"), 6 | comment = c(ORCID = "0000-0002-7111-0077")), 7 | person("Winston", "Chang", , "winston@posit.co", role = "aut"), 8 | person("Barret", "Schloerke", , "barret@posit.co", role = "aut", 9 | comment = c(ORCID = "0000-0001-9986-114X")), 10 | person("Posit Software, PBC", role = c("cph", "fnd"), comment = c(ROR = "03wc8by49")) 11 | ) 12 | Description: An implementation of the 'Chrome DevTools Protocol', for 13 | controlling a headless Chrome web browser. 14 | License: MIT + file LICENSE 15 | URL: https://rstudio.github.io/chromote/, 16 | https://github.com/rstudio/chromote 17 | BugReports: https://github.com/rstudio/chromote/issues 18 | Imports: 19 | cli, 20 | curl, 21 | fastmap, 22 | jsonlite, 23 | later (>= 1.1.0), 24 | magrittr, 25 | processx, 26 | promises (>= 1.1.1), 27 | R6, 28 | rlang (>= 1.1.0), 29 | utils, 30 | websocket (>= 1.2.0), 31 | withr, 32 | zip 33 | Suggests: 34 | knitr, 35 | rmarkdown, 36 | showimage, 37 | testthat (>= 3.0.0) 38 | VignetteBuilder: 39 | knitr 40 | Config/Needs/website: r-lib/pkgdown, rstudio/bslib 41 | Config/testthat/edition: 3 42 | Config/testthat/parallel: FALSE 43 | Config/testthat/start-first: chromote_session 44 | Encoding: UTF-8 45 | Language: en-US 46 | Roxygen: list(markdown = TRUE) 47 | RoxygenNote: 7.3.2 48 | SystemRequirements: Google Chrome or other Chromium-based browser. 49 | chromium: chromium (rpm) or chromium-browser (deb) 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2025 2 | COPYRIGHT HOLDER: chromote authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 chromote 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 | S3method(print,chromote_info) 4 | export("%...!%") 5 | export("%...>%") 6 | export("%...T!%") 7 | export("%...T>%") 8 | export("%>%") 9 | export("%T>%") 10 | export(Browser) 11 | export(Chrome) 12 | export(ChromeRemote) 13 | export(Chromote) 14 | export(ChromoteSession) 15 | export(catch) 16 | export(chrome_versions_add) 17 | export(chrome_versions_list) 18 | export(chrome_versions_path) 19 | export(chrome_versions_path_cache) 20 | export(chrome_versions_remove) 21 | export(chromote_info) 22 | export(default_chrome_args) 23 | export(default_chromote_object) 24 | export(finally) 25 | export(find_chrome) 26 | export(get_chrome_args) 27 | export(has_default_chromote_object) 28 | export(local_chrome_version) 29 | export(local_chromote_chrome) 30 | export(promise) 31 | export(set_chrome_args) 32 | export(set_default_chromote_object) 33 | export(then) 34 | export(with_chrome_version) 35 | export(with_chromote_chrome) 36 | import(later) 37 | import(promises) 38 | import(rlang) 39 | importFrom(R6,R6Class) 40 | importFrom(fastmap,fastmap) 41 | importFrom(jsonlite,fromJSON) 42 | importFrom(jsonlite,toJSON) 43 | importFrom(magrittr,"%>%") 44 | importFrom(magrittr,"%T>%") 45 | importFrom(processx,process) 46 | importFrom(promises,"%...!%") 47 | importFrom(promises,"%...>%") 48 | importFrom(promises,"%...T!%") 49 | importFrom(promises,"%...T>%") 50 | importFrom(promises,catch) 51 | importFrom(promises,finally) 52 | importFrom(promises,promise) 53 | importFrom(promises,then) 54 | importFrom(websocket,WebSocket) 55 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # chromote (development version) 2 | 3 | # chromote 0.5.1 4 | 5 | ## New features 6 | 7 | * `ChromoteSession` gets a new helper method, `$go_to()`. This is an easier way of reliably waiting for a page load, instead of using `Page$loadEventFired()` and `Page$navigate()` together. (#221) 8 | 9 | * `ChromoteSession$view()` now accommodates the new DevTools Frontend URL used by Chrome v135 and later (#225, #226). 10 | 11 | # chromote 0.5.0 12 | 13 | ## New features 14 | 15 | * chromote now includes experimental features to download versioned binaries of Chrome and `chrome-headless-shell` for Mac (x64 or arm64), Windows (32- or 64-bit) or Linux (x86-64) from the [Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/) service. (#198) 16 | * Use `with_chrome_version()` or `local_chrome_version()` to temporarily switch to a specific version of Chrome. The appropriate binary will be downloaded automatically if not yet available locally. 17 | * Use `chrome_versions_list()` to list installed or available versions of Chrome. 18 | * Or use `chrome_versions_add()` and `chrome_versions_remove()` to manually add or remove a specific version of Chrome from chromote's cache. 19 | 20 | * `ChromoteSession` gains two new helper methods: `$set_viewport_size()` and `$get_viewport_size()`. These methods allow you to change the viewport size – effectively the virtual window size for a page – or to get the current viewport size. If you previously relied on `$Emulation$setVisibleSize()` (now a deprecated method in the Chrome DevTools Protocol), `$set_viewport_size()` is a good replacement as it uses [Emulation.setDeviceMetricsOverride](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride) instead. (#206) 21 | 22 | ## Improvements 23 | 24 | * `ChromoteSession$new()` gains a `mobile` argument that can be used to set the device emulation in that session to emulate a mobile browser. The default is `mobile = FALSE`, which matches previous behavior. (#205) 25 | 26 | * `Chromote` and `ChromoteSesssion` gain an `$auto_events_enable_args()` method that sets that arguments used by chromote's auto-events feature when calling the `enable` command for a domain, e.g. `Fetch.enable`. (#208) 27 | 28 | * The `$view()` method of a `ChromoteSession` will now detect when `chrome-headless-shell` is being used and will use the system browser (via `utils::browseURL()`) rather than the Chrome instance attached to chromote. (#214) 29 | 30 | * chromote now has a hex sticker! Thank you to @davidrsch for the inspiration. (#216) 31 | 32 | ## Bug fixes 33 | 34 | * `ChromoteSession$new()` now sets `width` and `height` using [Emulation.setDeviceMetricsOverride](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride), which works for all Chrome binaries and versions. This fixes an issue with `width` and `height` being ignored for Chrome versions 128-133. (#205) 35 | 36 | * Fixed a bug in `chromote_info()` on Windows with Powershell when no version info is returned. (#207) 37 | 38 | * `Chromote` and `ChromoteSession` once again correctly handles connections to remote Chrome browsers via `ChromeRemote`. Calling `$close()` on a `Chromote` object connected to a remote browser no longer attempts to close the browser, and will now simply close the websocket connection to the browser. For local process, the `Chromote$close()` gains a `wait` argument that sets the number of seconds to wait for Chrome to gracefully shut down before chromote closes the process. (#212) 39 | 40 | # chromote 0.4.0 41 | 42 | * Chrome v132 and later no longer support [old headless mode](https://developer.chrome.com/blog/removing-headless-old-from-chrome). As such, `chromote` no longer defaults to using `--headless=old` and now uses `--headless` when running Chrome. You can still use the `chromote.headless` option or `CHROMOTE_HEADLESS` environment variable to configure the `--headless` flag if you're using an older version of Chrome. (#187) 43 | 44 | * Added `chromote_info()`, a new utility function to print out key information about chromote and Chrome. Useful when debugging chromote or reporting an issue. (#190) 45 | 46 | * chromote now uses a consistent prefix for logs, e.g `{tempdir}/chrome-{id}-stdout.log` and `{tempdir}/chrome-{id}-stderr.log`. chromote also now uses `--crash-dumps-dir` to set a session-specific temp directory. (#194) 47 | 48 | # chromote 0.3.1 49 | 50 | * Fixed a typo that caused `launch_chrome()` to throw an error. (#175) 51 | 52 | # chromote 0.3.0 53 | 54 | * The headless mode used by Chrome can now be selected with the `chromote.headless` option or `CHROMOTE_HEADLESS` environment variable. 55 | 56 | In Chrome v128, a [new headless mode](https://developer.chrome.com/docs/chromium/new-headless) became the default. The new mode uses the same browser engine as the regular Chrome browser, whereas the old headless mode is built on a separate architecture. The old headless mode may be faster to launch and is still well-suited to many of the tasks for which chromote is used. 57 | 58 | For now, to avoid disruption, chromote defaults to using the old headless mode. In the future, chromote will follow Chrome and default to `"new"` headless mode. (And at some point, Chrome intends to remove the old headless mode which is now offered as [a separate binary](https://developer.chrome.com/blog/chrome-headless-shell).) To test the new headless mode, use `options(chromote.headless = "new")` or `CHROMOTE_HEADLESS="new"` (in `.Renviron` or via `Sys.setenv()`). (#172) 59 | 60 | # chromote 0.2.0 61 | 62 | ## Breaking changes 63 | 64 | * Breaking change: `Chromote$is_active()` method now reports if there is an active connection to the underlying chrome instance, rather than whether or not that instance is alive (#94). 65 | 66 | ## Improvements and bug fixes 67 | 68 | * `Chromote` and `ChromoteSession` gain print methods to give you a snapshot of the most important values (#140). 69 | 70 | * `Chromote` gains a new `is_alive()` method equivalent to the old `is_active()` method; i.e. it reports on if there is an active chrome process running in the background (#136). 71 | 72 | * `ChromoteSession` now records the `targetId`. This eliminates one round-trip to the browser when viewing or closing a session. You can now call the `$respawn()` method if a session terminates and you want to reconnect to the same target (#94). 73 | 74 | * `ChromoteSession$screenshot()` gains an `options` argument that accepts a list of additional options to be passed to the Chrome Devtools Protocol's [`Page.captureScreenshot` method](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot) (#129). 75 | 76 | * `ChromoteSession$screenshot()` will now infer the image format from the `filename` extension. Alternatively, you can specify the `format` in the list passed to `options` (#130). 77 | 78 | * `--disable-gpu` is no longer included in the default Chrome arguments, except on windows where empirically it appears to be necessary (otherwise GHA check runs never terminate) (#142). 79 | 80 | # chromote 0.1.2 81 | 82 | * Fixed #109: An error would occur when a `Chromote` object's `$close()` method was called. (#110) 83 | 84 | * Fixed #99: When the `$view()` method was called, recent versions of Chrome would display `"Debugging connection was closed. Reason: WebSocket disconnected"`. (#101) 85 | 86 | * Fixed #89, #91: `find_chrome()` now checks more possible binary names for Chrome or Chromium on Linux and Mac. (thanks @brianmsm and @rossellhayes, #117) 87 | 88 | * Fixed #22: Added a new `chromote.timeout` global option that can be used to set the timeout (in seconds) for establishing connections with the Chrome session. (#120) 89 | 90 | 91 | # chromote 0.1.1 92 | 93 | * Update docs for CRAN (#93) 94 | 95 | 96 | # chromote 0.1.0 97 | 98 | * Initial package release 99 | -------------------------------------------------------------------------------- /R/browser.R: -------------------------------------------------------------------------------- 1 | globals <- new.env() 2 | 3 | #' Browser base class 4 | #' 5 | #' @description 6 | #' Base class for browsers like Chrome, Chromium, etc. Defines the interface 7 | #' used by various browser implementations. It can represent a local browser 8 | #' process or one running remotely. 9 | #' 10 | #' @details 11 | #' The `initialize()` method of an implementation should set `private$host` 12 | #' and `private$port`. If the process is local, the `initialize()` method 13 | #' should also set `private$process`. 14 | #' 15 | #' @export 16 | Browser <- R6Class( 17 | "Browser", 18 | public = list( 19 | # Returns TRUE if the browser is running locally, FALSE if it's remote. 20 | #' @description Is local browser? 21 | #' Returns TRUE if the browser is running locally, FALSE if it's remote. 22 | is_local = function() !is.null(private$process), 23 | 24 | #' @description Browser process 25 | get_process = function() private$process, 26 | 27 | #' @description Is the process alive? 28 | is_alive = function() private$process$is_alive(), 29 | 30 | #' @description Browser Host 31 | get_host = function() private$host, 32 | 33 | #' @description Browser port 34 | get_port = function() private$port, 35 | 36 | #' @description Close the browser 37 | #' @param wait If an integer, waits a number of seconds for the process to 38 | #' exit, killing the process if it takes longer than `wait` seconds to 39 | #' close. Use `wait = TRUE` to wait for 10 seconds. 40 | close = function(wait = FALSE) { 41 | if (!self$is_local()) return(invisible()) 42 | if (!private$process$is_alive()) return(invisible()) 43 | 44 | if (!isFALSE(wait)) { 45 | if (isTRUE(wait)) wait <- 10 46 | check_number_whole(wait, min = 0) 47 | } 48 | 49 | private$process$signal(tools::SIGTERM) 50 | 51 | if (!isFALSE(wait)) { 52 | tryCatch( 53 | { 54 | private$process$wait(timeout = wait * 1000) 55 | if (private$process$is_alive()) { 56 | stop("shut it down") # ignored, used to escalate 57 | } 58 | }, 59 | error = function(err) { 60 | # Still alive after wait... 61 | try(private$process$kill(), silent = TRUE) 62 | } 63 | ) 64 | } 65 | } 66 | ), 67 | private = list( 68 | process = NULL, 69 | host = NULL, 70 | port = NULL, 71 | finalize = function(e) { 72 | if (self$is_local()) { 73 | self$close() 74 | } 75 | } 76 | ) 77 | ) 78 | -------------------------------------------------------------------------------- /R/callbacks.R: -------------------------------------------------------------------------------- 1 | # The data structure for storing callbacks is essentially a queue: items are 2 | # added to the end, and removed from the front. Occasionally a callback will 3 | # be manually removed from the middle of the queue. For each callback that's 4 | # registered, we provide a function that can remove that callback from the 5 | # queue. 6 | Callbacks <- R6Class( 7 | "Callbacks", 8 | public = list( 9 | initialize = function() { 10 | # Use floating point because it has greater range than int while 11 | # maintaining precision of 1.0. 12 | private$nextId <- 1.0 13 | private$callbacks <- fastmap() 14 | }, 15 | add = function(callback) { 16 | if (!is.function(callback)) { 17 | stop("callback must be a function.") 18 | } 19 | 20 | # Keys are formatted like "0000000000001", "0000000000002", etc., so 21 | # that they can be easily sorted by numerical value. 22 | id <- sprintf("%013.f", private$nextId) 23 | private$nextId <- private$nextId + 1.0 24 | private$callbacks$set(id, callback) 25 | 26 | # Return function for unregistering the callback. 27 | invisible(function() { 28 | if (private$callbacks$has(id)) { 29 | private$callbacks$remove(id) 30 | } 31 | }) 32 | }, 33 | invoke = function(..., on_error = NULL) { 34 | # Ensure that calls are invoked in the order that they were registered 35 | keys <- private$callbacks$keys(sort = TRUE) 36 | 37 | errors <- character() 38 | if (is.null(on_error)) { 39 | on_error <- function(e) { 40 | errors[length(errors) + 1] <<- e$message 41 | } 42 | } 43 | 44 | for (key in keys) { 45 | callback <- private$callbacks$get(key) 46 | tryCatch(callback(...), error = on_error) 47 | } 48 | 49 | if (length(errors) != 0) { 50 | warning( 51 | paste0( 52 | length(errors), 53 | " errors occurred while executing callbacks:\n ", 54 | paste(errors, collapse = "\n ") 55 | ) 56 | ) 57 | } 58 | }, 59 | clear = function() { 60 | private$callbacks <- fastmap() 61 | }, 62 | size = function() { 63 | private$callbacks$size() 64 | } 65 | ), 66 | private = list( 67 | nextId = NULL, 68 | callbacks = NULL 69 | ) 70 | ) 71 | -------------------------------------------------------------------------------- /R/chromote-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | #' chromote Options 5 | #' 6 | #' @description 7 | #' These options and environment variables that are used by chromote. Options 8 | #' are lowercase and can be set with `options()`. Environment variables are 9 | #' uppercase and can be set in an `.Renviron` file, with `Sys.setenv()`, or in 10 | #' the shell or process running R. If both an option or environment variable are 11 | #' supported, chromote will use the option first. 12 | #' 13 | #' * `CHROMOTE_CHROME` \cr 14 | #' Path to the Chrome executable. If not set, chromote will 15 | #' attempt to find and use the system installation of Chrome. 16 | #' * `chromote.headless`, `CHROMOTE_HEADLESS` \cr 17 | #' Headless mode for Chrome. Can be `"old"` or `"new"`. See 18 | #' [Chrome Headless mode](https://developer.chrome.com/docs/chromium/new-headless) 19 | #' for more details. 20 | #' * `chromote.timeout` \cr 21 | #' Timeout (in seconds) for Chrome to launch or connect. Default is `10`. 22 | #' * `chromote.launch.echo_cmd` \cr 23 | #' Echo the command used to launch Chrome to the console for debugging. 24 | #' Default is `FALSE`. 25 | #' 26 | #' @name chromote-options 27 | NULL 28 | 29 | ## usethis namespace: start 30 | #' @import promises later rlang 31 | #' @importFrom fastmap fastmap 32 | #' @importFrom jsonlite fromJSON toJSON 33 | #' @importFrom processx process 34 | #' @importFrom R6 R6Class 35 | #' @importFrom websocket WebSocket 36 | ## usethis namespace: end 37 | NULL 38 | 39 | # inlined from `lifecycle::badge()` and only supports the experimental badge. 40 | # Use `usethis::use_lifecycle()` to add additional badges. 41 | lifecycle_badge <- function(stage) { 42 | stage <- rlang::arg_match0( 43 | stage, 44 | c("experimental") #, "stable", "superseded", "deprecated") 45 | ) 46 | stage_name <- substr(stage, 1, 1) <- toupper(substr(stage, 1, 1)) 47 | 48 | url <- paste0("https://lifecycle.r-lib.org/articles/stages.html#", stage) 49 | 50 | html <- sprintf( 51 | "\\href{%s}{\\figure{%s}{options: alt='[%s]'}}", 52 | url, 53 | file.path(tolower(sprintf("lifecycle-%s.svg", stage))), 54 | stage_name 55 | ) 56 | 57 | text <- sprintf("\\strong{[%s]}", stage_name) 58 | sprintf("\\ifelse{html}{%s}{%s}", html, text) 59 | } 60 | -------------------------------------------------------------------------------- /R/event_manager.R: -------------------------------------------------------------------------------- 1 | EventManager <- R6Class( 2 | "EventManager", 3 | public = list( 4 | initialize = function(session) { 5 | private$session <- session 6 | 7 | if (length(session$protocol) == 0) { 8 | stop("Session object must have non-empty protocol field.") 9 | } 10 | 11 | # Find out which domains require the .enable command to enable 12 | # event notifications. 13 | private$event_enable_domains <- lapply( 14 | session$protocol, 15 | function(domain) { 16 | is.function(domain$enable) 17 | } 18 | ) 19 | 20 | private$event_callbacks <- fastmap() 21 | }, 22 | 23 | register_event_listener = function(event, callback = NULL, timeout = NULL) { 24 | domain <- find_domain(event) 25 | 26 | # Note: If callback is specified, then timeout is ignored. Also, returns 27 | # a function for deregistering the callback, instead of a promise. 28 | if (!is.null(callback)) { 29 | deregister_callback_fn <- private$add_event_callback( 30 | event, 31 | callback, 32 | once = FALSE 33 | ) 34 | return(invisible(deregister_callback_fn)) 35 | } 36 | 37 | deregister_callback_fn <- NULL 38 | p <- promise(function(resolve, reject) { 39 | deregister_callback_fn <<- private$add_event_callback( 40 | event, 41 | resolve, 42 | once = TRUE 43 | ) 44 | }) 45 | 46 | if (!is.null(timeout) && !is.infinite(timeout)) { 47 | # !!! TODO: Fix loop !!! 48 | p <- promise_timeout( 49 | p, 50 | timeout, 51 | loop = private$session$get_child_loop(), 52 | timeout_message = paste0( 53 | "Chromote: timed out waiting for event ", 54 | event 55 | ) 56 | ) 57 | } 58 | 59 | p <- p$finally(function() { 60 | deregister_callback_fn() 61 | }) 62 | p 63 | }, 64 | 65 | invoke_event_callbacks = function(event, params) { 66 | callbacks <- private$event_callbacks$get(event) 67 | if (is.null(callbacks) || callbacks$size() == 0) return() 68 | 69 | callbacks$invoke(params) 70 | }, 71 | 72 | remove_event_callbacks = function(event) { 73 | # Removes ALL callbacks for a given event. In the future it might be 74 | # useful to implement finer control. 75 | private$event_callbacks$remove(event) 76 | } 77 | ), 78 | 79 | private = list( 80 | # The ChromoteSession or Chromote object that owns this EventManager. 81 | session = NULL, 82 | event_callbacks = NULL, 83 | # For keeping count of the number of callbacks for each domain; if 84 | # auto_events is TRUE, then when the count goes from 0 to 1 or 1 to 0 for 85 | # a given domain, it will automatically enable or disable events for that 86 | # domain. 87 | event_callback_counts = list(), 88 | 89 | # Some domains require a .event command to enable event 90 | # notifications, others do not. (Not really sure why.) 91 | event_enable_domains = NULL, 92 | 93 | add_event_callback = function(event, callback, once) { 94 | if (!private$event_callbacks$has(event)) { 95 | private$event_callbacks$set(event, Callbacks$new()) 96 | } 97 | 98 | if (once) { 99 | orig_callback <- callback 100 | callback <- function(...) { 101 | tryCatch( 102 | orig_callback(...), 103 | finally = deregister_and_dec() 104 | ) 105 | } 106 | } 107 | 108 | deregister_callback <- private$event_callbacks$get(event)$add(callback) 109 | 110 | domain <- find_domain(event) 111 | private$inc_event_callback_count(domain) 112 | 113 | # We'll wrap deregister_callback in another function which also keeps 114 | # count to the number of callbacks for the domain. 115 | deregister_called <- FALSE 116 | deregister_and_dec <- function() { 117 | # Make sure that if this is called multiple times that it doesn't keep 118 | # having effects. 119 | if (deregister_called) return() 120 | deregister_called <<- TRUE 121 | 122 | deregister_callback() 123 | private$dec_event_callback_count(domain) 124 | } 125 | 126 | deregister_and_dec 127 | }, 128 | 129 | inc_event_callback_count = function(domain) { 130 | if (is.null(private$event_callback_counts[[domain]])) { 131 | private$event_callback_counts[[domain]] <- 0 132 | } 133 | 134 | private$event_callback_counts[[domain]] <- 135 | private$event_callback_counts[[domain]] + 1 136 | 137 | private$session$debug_log( 138 | "Callbacks for ", 139 | domain, 140 | "++: ", 141 | private$event_callback_counts[[domain]] 142 | ) 143 | 144 | # If we're doing auto events and we're going from 0 to 1, enable events 145 | # for this domain. (Some domains do not require or have an .enable 146 | # method.) 147 | if ( 148 | private$session$get_auto_events() && 149 | private$event_callback_counts[[domain]] == 1 && 150 | isTRUE(private$event_enable_domains[[domain]]) 151 | ) { 152 | private$session$debug_log("Enabling events for ", domain) 153 | args <- private$session$auto_events_enable_args(domain) 154 | exec( 155 | private$session[[domain]]$enable, 156 | !!!args 157 | ) 158 | } 159 | 160 | invisible(private$event_callback_counts[[domain]]) 161 | }, 162 | 163 | dec_event_callback_count = function(domain) { 164 | private$event_callback_counts[[domain]] <- 165 | private$event_callback_counts[[domain]] - 1 166 | 167 | private$session$debug_log( 168 | "Callbacks for ", 169 | domain, 170 | "--: ", 171 | private$event_callback_counts[[domain]] 172 | ) 173 | # If we're doing auto events and we're going from 1 to 0, disable 174 | # enable events for this domain. 175 | if ( 176 | private$session$get_auto_events() && 177 | private$event_callback_counts[[domain]] == 0 && 178 | isTRUE(private$event_enable_domains[[domain]]) 179 | ) { 180 | private$session$debug_log("Disabling events for ", domain) 181 | private$session[[domain]]$disable() 182 | } 183 | 184 | invisible(private$event_callback_counts[[domain]]) 185 | } 186 | ) 187 | ) 188 | 189 | # These functions power `$auto_events_enable_args()` for both `Chromote` and 190 | # `ChromoteSession`. 191 | get_auto_events_enable_args <- function(private, domain, parent = NULL) { 192 | session_args <- private$auto_events_enable_args[[domain]] 193 | if (!is.null(session_args) || is.null(parent)) { 194 | return(session_args) 195 | } 196 | 197 | return(parent$auto_events_enable_args(domain)) 198 | } 199 | 200 | set_auto_events_enable_args <- function(self, private, domain, dots) { 201 | # Set enable args for the domain ---- 202 | if (identical(dots, list("NULL" = NULL))) { 203 | # Unset args with `$auto_events_enable_args(domain, NULL)` 204 | dots <- NULL 205 | } 206 | 207 | if (!is_function(self[[domain]]$enable)) { 208 | cli::cli_abort( 209 | "{.field {domain}} does not have an {.field enable} method.", 210 | call = parent.frame() 211 | ) 212 | } 213 | 214 | known_args <- names(fn_fmls(self[[domain]]$enable)) 215 | unknown_args <- setdiff(names(dots), known_args) 216 | if (length(unknown_args)) { 217 | cli::cli_abort( 218 | c( 219 | "{.field {domain}.enable} does not have {cli::qty(unknown_args)}argument{?s}: {.arg {unknown_args}}.", 220 | "i" = "Available arguments: {.arg {setdiff(known_args, 'wait_')}}" 221 | ), 222 | call = parent.frame() 223 | ) 224 | } 225 | 226 | if ("wait_" %in% names(dots)) { 227 | cli::cli_warn( 228 | "{.arg wait_} cannot be set for {.field {domain}.enable}, ignoring.", 229 | call = parent.frame() 230 | ) 231 | dots[["wait_"]] <- NULL 232 | } 233 | 234 | old <- self$auto_events_enable_args(domain) 235 | private$auto_events_enable_args[[domain]] <- dots 236 | invisible(old) 237 | } 238 | -------------------------------------------------------------------------------- /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: 2024-02-14 9 | # license: https://unlicense.org 10 | # imports: rlang (>= 1.1.0) 11 | # --- 12 | # 13 | # ## Changelog 14 | # 15 | # 2024-02-14: 16 | # - `obj_type_friendly()` now works for S7 objects. 17 | # 18 | # 2023-05-01: 19 | # - `obj_type_friendly()` now only displays the first class of S3 objects. 20 | # 21 | # 2023-03-30: 22 | # - `stop_input_type()` now handles `I()` input literally in `arg`. 23 | # 24 | # 2022-10-04: 25 | # - `obj_type_friendly(value = TRUE)` now shows numeric scalars 26 | # literally. 27 | # - `stop_friendly_type()` now takes `show_value`, passed to 28 | # `obj_type_friendly()` as the `value` argument. 29 | # 30 | # 2022-10-03: 31 | # - Added `allow_na` and `allow_null` arguments. 32 | # - `NULL` is now backticked. 33 | # - Better friendly type for infinities and `NaN`. 34 | # 35 | # 2022-09-16: 36 | # - Unprefixed usage of rlang functions with `rlang::` to 37 | # avoid onLoad issues when called from rlang (#1482). 38 | # 39 | # 2022-08-11: 40 | # - Prefixed usage of rlang functions with `rlang::`. 41 | # 42 | # 2022-06-22: 43 | # - `friendly_type_of()` is now `obj_type_friendly()`. 44 | # - Added `obj_type_oo()`. 45 | # 46 | # 2021-12-20: 47 | # - Added support for scalar values and empty vectors. 48 | # - Added `stop_input_type()` 49 | # 50 | # 2021-06-30: 51 | # - Added support for missing arguments. 52 | # 53 | # 2021-04-19: 54 | # - Added support for matrices and arrays (#141). 55 | # - Added documentation. 56 | # - Added changelog. 57 | # 58 | # nocov start 59 | 60 | #' Return English-friendly type 61 | #' @param x Any R object. 62 | #' @param value Whether to describe the value of `x`. Special values 63 | #' like `NA` or `""` are always described. 64 | #' @param length Whether to mention the length of vectors and lists. 65 | #' @return A string describing the type. Starts with an indefinite 66 | #' article, e.g. "an integer vector". 67 | #' @noRd 68 | obj_type_friendly <- function(x, value = TRUE) { 69 | if (is_missing(x)) { 70 | return("absent") 71 | } 72 | 73 | if (is.object(x)) { 74 | if (inherits(x, "quosure")) { 75 | type <- "quosure" 76 | } else { 77 | type <- class(x)[[1L]] 78 | } 79 | return(sprintf("a <%s> object", type)) 80 | } 81 | 82 | if (!is_vector(x)) { 83 | return(.rlang_as_friendly_type(typeof(x))) 84 | } 85 | 86 | n_dim <- length(dim(x)) 87 | 88 | if (!n_dim) { 89 | if (!is_list(x) && length(x) == 1) { 90 | if (is_na(x)) { 91 | return(switch( 92 | typeof(x), 93 | logical = "`NA`", 94 | integer = "an integer `NA`", 95 | double = 96 | if (is.nan(x)) { 97 | "`NaN`" 98 | } else { 99 | "a numeric `NA`" 100 | }, 101 | complex = "a complex `NA`", 102 | character = "a character `NA`", 103 | .rlang_stop_unexpected_typeof(x) 104 | )) 105 | } 106 | 107 | show_infinites <- function(x) { 108 | if (x > 0) { 109 | "`Inf`" 110 | } else { 111 | "`-Inf`" 112 | } 113 | } 114 | str_encode <- function(x, width = 30, ...) { 115 | if (nchar(x) > width) { 116 | x <- substr(x, 1, width - 3) 117 | x <- paste0(x, "...") 118 | } 119 | encodeString(x, ...) 120 | } 121 | 122 | if (value) { 123 | if (is.numeric(x) && is.infinite(x)) { 124 | return(show_infinites(x)) 125 | } 126 | 127 | if (is.numeric(x) || is.complex(x)) { 128 | number <- as.character(round(x, 2)) 129 | what <- if (is.complex(x)) "the complex number" else "the number" 130 | return(paste(what, number)) 131 | } 132 | 133 | return(switch( 134 | typeof(x), 135 | logical = if (x) "`TRUE`" else "`FALSE`", 136 | character = { 137 | what <- if (nzchar(x)) "the string" else "the empty string" 138 | paste(what, str_encode(x, quote = "\"")) 139 | }, 140 | raw = paste("the raw value", as.character(x)), 141 | .rlang_stop_unexpected_typeof(x) 142 | )) 143 | } 144 | 145 | return(switch( 146 | typeof(x), 147 | logical = "a logical value", 148 | integer = "an integer", 149 | double = if (is.infinite(x)) show_infinites(x) else "a number", 150 | complex = "a complex number", 151 | character = if (nzchar(x)) "a string" else "\"\"", 152 | raw = "a raw value", 153 | .rlang_stop_unexpected_typeof(x) 154 | )) 155 | } 156 | 157 | if (length(x) == 0) { 158 | return(switch( 159 | typeof(x), 160 | logical = "an empty logical vector", 161 | integer = "an empty integer vector", 162 | double = "an empty numeric vector", 163 | complex = "an empty complex vector", 164 | character = "an empty character vector", 165 | raw = "an empty raw vector", 166 | list = "an empty list", 167 | .rlang_stop_unexpected_typeof(x) 168 | )) 169 | } 170 | } 171 | 172 | vec_type_friendly(x) 173 | } 174 | 175 | vec_type_friendly <- function(x, length = FALSE) { 176 | if (!is_vector(x)) { 177 | abort("`x` must be a vector.") 178 | } 179 | type <- typeof(x) 180 | n_dim <- length(dim(x)) 181 | 182 | add_length <- function(type) { 183 | if (length && !n_dim) { 184 | paste0(type, sprintf(" of length %s", length(x))) 185 | } else { 186 | type 187 | } 188 | } 189 | 190 | if (type == "list") { 191 | if (n_dim < 2) { 192 | return(add_length("a list")) 193 | } else if (is.data.frame(x)) { 194 | return("a data frame") 195 | } else if (n_dim == 2) { 196 | return("a list matrix") 197 | } else { 198 | return("a list array") 199 | } 200 | } 201 | 202 | type <- switch( 203 | type, 204 | logical = "a logical %s", 205 | integer = "an integer %s", 206 | numeric = , 207 | double = "a double %s", 208 | complex = "a complex %s", 209 | character = "a character %s", 210 | raw = "a raw %s", 211 | type = paste0("a ", type, " %s") 212 | ) 213 | 214 | if (n_dim < 2) { 215 | kind <- "vector" 216 | } else if (n_dim == 2) { 217 | kind <- "matrix" 218 | } else { 219 | kind <- "array" 220 | } 221 | out <- sprintf(type, kind) 222 | 223 | if (n_dim >= 2) { 224 | out 225 | } else { 226 | add_length(out) 227 | } 228 | } 229 | 230 | .rlang_as_friendly_type <- function(type) { 231 | switch( 232 | type, 233 | 234 | list = "a list", 235 | 236 | NULL = "`NULL`", 237 | environment = "an environment", 238 | externalptr = "a pointer", 239 | weakref = "a weak reference", 240 | S4 = "an S4 object", 241 | 242 | name = , 243 | symbol = "a symbol", 244 | language = "a call", 245 | pairlist = "a pairlist node", 246 | expression = "an expression vector", 247 | 248 | char = "an internal string", 249 | promise = "an internal promise", 250 | ... = "an internal dots object", 251 | any = "an internal `any` object", 252 | bytecode = "an internal bytecode object", 253 | 254 | primitive = , 255 | builtin = , 256 | special = "a primitive function", 257 | closure = "a function", 258 | 259 | type 260 | ) 261 | } 262 | 263 | .rlang_stop_unexpected_typeof <- function(x, call = caller_env()) { 264 | abort( 265 | sprintf("Unexpected type <%s>.", typeof(x)), 266 | call = call 267 | ) 268 | } 269 | 270 | #' Return OO type 271 | #' @param x Any R object. 272 | #' @return One of `"bare"` (for non-OO objects), `"S3"`, `"S4"`, 273 | #' `"R6"`, or `"S7"`. 274 | #' @noRd 275 | obj_type_oo <- function(x) { 276 | if (!is.object(x)) { 277 | return("bare") 278 | } 279 | 280 | class <- inherits(x, c("R6", "S7_object"), which = TRUE) 281 | 282 | if (class[[1]]) { 283 | "R6" 284 | } else if (class[[2]]) { 285 | "S7" 286 | } else if (isS4(x)) { 287 | "S4" 288 | } else { 289 | "S3" 290 | } 291 | } 292 | 293 | #' @param x The object type which does not conform to `what`. Its 294 | #' `obj_type_friendly()` is taken and mentioned in the error message. 295 | #' @param what The friendly expected type as a string. Can be a 296 | #' character vector of expected types, in which case the error 297 | #' message mentions all of them in an "or" enumeration. 298 | #' @param show_value Passed to `value` argument of `obj_type_friendly()`. 299 | #' @param ... Arguments passed to [abort()]. 300 | #' @inheritParams args_error_context 301 | #' @noRd 302 | stop_input_type <- function(x, 303 | what, 304 | ..., 305 | allow_na = FALSE, 306 | allow_null = FALSE, 307 | show_value = TRUE, 308 | arg = caller_arg(x), 309 | call = caller_env()) { 310 | # From standalone-cli.R 311 | cli <- env_get_list( 312 | nms = c("format_arg", "format_code"), 313 | last = topenv(), 314 | default = function(x) sprintf("`%s`", x), 315 | inherit = TRUE 316 | ) 317 | 318 | if (allow_na) { 319 | what <- c(what, cli$format_code("NA")) 320 | } 321 | if (allow_null) { 322 | what <- c(what, cli$format_code("NULL")) 323 | } 324 | if (length(what)) { 325 | what <- oxford_comma(what) 326 | } 327 | if (inherits(arg, "AsIs")) { 328 | format_arg <- identity 329 | } else { 330 | format_arg <- cli$format_arg 331 | } 332 | 333 | message <- sprintf( 334 | "%s must be %s, not %s.", 335 | format_arg(arg), 336 | what, 337 | obj_type_friendly(x, value = show_value) 338 | ) 339 | 340 | abort(message, ..., call = call, arg = arg) 341 | } 342 | 343 | oxford_comma <- function(chr, sep = ", ", final = "or") { 344 | n <- length(chr) 345 | 346 | if (n < 2) { 347 | return(chr) 348 | } 349 | 350 | head <- chr[seq_len(n - 1)] 351 | last <- chr[n] 352 | 353 | head <- paste(head, collapse = sep) 354 | 355 | # Write a or b. But a, b, or c. 356 | if (n > 2) { 357 | paste0(head, sep, final, " ", last) 358 | } else { 359 | paste0(head, " ", final, " ", last) 360 | } 361 | } 362 | 363 | # nocov end 364 | -------------------------------------------------------------------------------- /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 | # 2024-08-15: 17 | # - `check_character()` gains an `allow_na` argument (@martaalcalde, #1724) 18 | # 19 | # 2023-03-13: 20 | # - Improved error messages of number checkers (@teunbrand) 21 | # - Added `allow_infinite` argument to `check_number_whole()` (@mgirlich). 22 | # - Added `check_data_frame()` (@mgirlich). 23 | # 24 | # 2023-03-07: 25 | # - Added dependency on rlang (>= 1.1.0). 26 | # 27 | # 2023-02-15: 28 | # - Added `check_logical()`. 29 | # 30 | # - `check_bool()`, `check_number_whole()`, and 31 | # `check_number_decimal()` are now implemented in C. 32 | # 33 | # - For efficiency, `check_number_whole()` and 34 | # `check_number_decimal()` now take a `NULL` default for `min` and 35 | # `max`. This makes it possible to bypass unnecessary type-checking 36 | # and comparisons in the default case of no bounds checks. 37 | # 38 | # 2022-10-07: 39 | # - `check_number_whole()` and `_decimal()` no longer treat 40 | # non-numeric types such as factors or dates as numbers. Numeric 41 | # types are detected with `is.numeric()`. 42 | # 43 | # 2022-10-04: 44 | # - Added `check_name()` that forbids the empty string. 45 | # `check_string()` allows the empty string by default. 46 | # 47 | # 2022-09-28: 48 | # - Removed `what` arguments. 49 | # - Added `allow_na` and `allow_null` arguments. 50 | # - Added `allow_decimal` and `allow_infinite` arguments. 51 | # - Improved errors with absent arguments. 52 | # 53 | # 54 | # 2022-09-16: 55 | # - Unprefixed usage of rlang functions with `rlang::` to 56 | # avoid onLoad issues when called from rlang (#1482). 57 | # 58 | # 2022-08-11: 59 | # - Added changelog. 60 | # 61 | # nocov start 62 | 63 | # Scalars ----------------------------------------------------------------- 64 | 65 | .standalone_types_check_dot_call <- .Call 66 | 67 | check_bool <- function(x, 68 | ..., 69 | allow_na = FALSE, 70 | allow_null = FALSE, 71 | arg = caller_arg(x), 72 | call = caller_env()) { 73 | if (!missing(x) && .standalone_types_check_dot_call(ffi_standalone_is_bool_1.0.7, x, allow_na, allow_null)) { 74 | return(invisible(NULL)) 75 | } 76 | 77 | stop_input_type( 78 | x, 79 | c("`TRUE`", "`FALSE`"), 80 | ..., 81 | allow_na = allow_na, 82 | allow_null = allow_null, 83 | arg = arg, 84 | call = call 85 | ) 86 | } 87 | 88 | check_string <- function(x, 89 | ..., 90 | allow_empty = TRUE, 91 | allow_na = FALSE, 92 | allow_null = FALSE, 93 | arg = caller_arg(x), 94 | call = caller_env()) { 95 | if (!missing(x)) { 96 | is_string <- .rlang_check_is_string( 97 | x, 98 | allow_empty = allow_empty, 99 | allow_na = allow_na, 100 | allow_null = allow_null 101 | ) 102 | if (is_string) { 103 | return(invisible(NULL)) 104 | } 105 | } 106 | 107 | stop_input_type( 108 | x, 109 | "a single string", 110 | ..., 111 | allow_na = allow_na, 112 | allow_null = allow_null, 113 | arg = arg, 114 | call = call 115 | ) 116 | } 117 | 118 | .rlang_check_is_string <- function(x, 119 | allow_empty, 120 | allow_na, 121 | allow_null) { 122 | if (is_string(x)) { 123 | if (allow_empty || !is_string(x, "")) { 124 | return(TRUE) 125 | } 126 | } 127 | 128 | if (allow_null && is_null(x)) { 129 | return(TRUE) 130 | } 131 | 132 | if (allow_na && (identical(x, NA) || identical(x, na_chr))) { 133 | return(TRUE) 134 | } 135 | 136 | FALSE 137 | } 138 | 139 | check_name <- function(x, 140 | ..., 141 | allow_null = FALSE, 142 | arg = caller_arg(x), 143 | call = caller_env()) { 144 | if (!missing(x)) { 145 | is_string <- .rlang_check_is_string( 146 | x, 147 | allow_empty = FALSE, 148 | allow_na = FALSE, 149 | allow_null = allow_null 150 | ) 151 | if (is_string) { 152 | return(invisible(NULL)) 153 | } 154 | } 155 | 156 | stop_input_type( 157 | x, 158 | "a valid name", 159 | ..., 160 | allow_na = FALSE, 161 | allow_null = allow_null, 162 | arg = arg, 163 | call = call 164 | ) 165 | } 166 | 167 | IS_NUMBER_true <- 0 168 | IS_NUMBER_false <- 1 169 | IS_NUMBER_oob <- 2 170 | 171 | check_number_decimal <- function(x, 172 | ..., 173 | min = NULL, 174 | max = NULL, 175 | allow_infinite = TRUE, 176 | allow_na = FALSE, 177 | allow_null = FALSE, 178 | arg = caller_arg(x), 179 | call = caller_env()) { 180 | if (missing(x)) { 181 | exit_code <- IS_NUMBER_false 182 | } else if (0 == (exit_code <- .standalone_types_check_dot_call( 183 | ffi_standalone_check_number_1.0.7, 184 | x, 185 | allow_decimal = TRUE, 186 | min, 187 | max, 188 | allow_infinite, 189 | allow_na, 190 | allow_null 191 | ))) { 192 | return(invisible(NULL)) 193 | } 194 | 195 | .stop_not_number( 196 | x, 197 | ..., 198 | exit_code = exit_code, 199 | allow_decimal = TRUE, 200 | min = min, 201 | max = max, 202 | allow_na = allow_na, 203 | allow_null = allow_null, 204 | arg = arg, 205 | call = call 206 | ) 207 | } 208 | 209 | check_number_whole <- function(x, 210 | ..., 211 | min = NULL, 212 | max = NULL, 213 | allow_infinite = FALSE, 214 | allow_na = FALSE, 215 | allow_null = FALSE, 216 | arg = caller_arg(x), 217 | call = caller_env()) { 218 | if (missing(x)) { 219 | exit_code <- IS_NUMBER_false 220 | } else if (0 == (exit_code <- .standalone_types_check_dot_call( 221 | ffi_standalone_check_number_1.0.7, 222 | x, 223 | allow_decimal = FALSE, 224 | min, 225 | max, 226 | allow_infinite, 227 | allow_na, 228 | allow_null 229 | ))) { 230 | return(invisible(NULL)) 231 | } 232 | 233 | .stop_not_number( 234 | x, 235 | ..., 236 | exit_code = exit_code, 237 | allow_decimal = FALSE, 238 | min = min, 239 | max = max, 240 | allow_na = allow_na, 241 | allow_null = allow_null, 242 | arg = arg, 243 | call = call 244 | ) 245 | } 246 | 247 | .stop_not_number <- function(x, 248 | ..., 249 | exit_code, 250 | allow_decimal, 251 | min, 252 | max, 253 | allow_na, 254 | allow_null, 255 | arg, 256 | call) { 257 | if (allow_decimal) { 258 | what <- "a number" 259 | } else { 260 | what <- "a whole number" 261 | } 262 | 263 | if (exit_code == IS_NUMBER_oob) { 264 | min <- min %||% -Inf 265 | max <- max %||% Inf 266 | 267 | if (min > -Inf && max < Inf) { 268 | what <- sprintf("%s between %s and %s", what, min, max) 269 | } else if (x < min) { 270 | what <- sprintf("%s larger than or equal to %s", what, min) 271 | } else if (x > max) { 272 | what <- sprintf("%s smaller than or equal to %s", what, max) 273 | } else { 274 | abort("Unexpected state in OOB check", .internal = TRUE) 275 | } 276 | } 277 | 278 | stop_input_type( 279 | x, 280 | what, 281 | ..., 282 | allow_na = allow_na, 283 | allow_null = allow_null, 284 | arg = arg, 285 | call = call 286 | ) 287 | } 288 | 289 | check_symbol <- function(x, 290 | ..., 291 | allow_null = FALSE, 292 | arg = caller_arg(x), 293 | call = caller_env()) { 294 | if (!missing(x)) { 295 | if (is_symbol(x)) { 296 | return(invisible(NULL)) 297 | } 298 | if (allow_null && is_null(x)) { 299 | return(invisible(NULL)) 300 | } 301 | } 302 | 303 | stop_input_type( 304 | x, 305 | "a symbol", 306 | ..., 307 | allow_na = FALSE, 308 | allow_null = allow_null, 309 | arg = arg, 310 | call = call 311 | ) 312 | } 313 | 314 | check_arg <- function(x, 315 | ..., 316 | allow_null = FALSE, 317 | arg = caller_arg(x), 318 | call = caller_env()) { 319 | if (!missing(x)) { 320 | if (is_symbol(x)) { 321 | return(invisible(NULL)) 322 | } 323 | if (allow_null && is_null(x)) { 324 | return(invisible(NULL)) 325 | } 326 | } 327 | 328 | stop_input_type( 329 | x, 330 | "an argument name", 331 | ..., 332 | allow_na = FALSE, 333 | allow_null = allow_null, 334 | arg = arg, 335 | call = call 336 | ) 337 | } 338 | 339 | check_call <- function(x, 340 | ..., 341 | allow_null = FALSE, 342 | arg = caller_arg(x), 343 | call = caller_env()) { 344 | if (!missing(x)) { 345 | if (is_call(x)) { 346 | return(invisible(NULL)) 347 | } 348 | if (allow_null && is_null(x)) { 349 | return(invisible(NULL)) 350 | } 351 | } 352 | 353 | stop_input_type( 354 | x, 355 | "a defused call", 356 | ..., 357 | allow_na = FALSE, 358 | allow_null = allow_null, 359 | arg = arg, 360 | call = call 361 | ) 362 | } 363 | 364 | check_environment <- function(x, 365 | ..., 366 | allow_null = FALSE, 367 | arg = caller_arg(x), 368 | call = caller_env()) { 369 | if (!missing(x)) { 370 | if (is_environment(x)) { 371 | return(invisible(NULL)) 372 | } 373 | if (allow_null && is_null(x)) { 374 | return(invisible(NULL)) 375 | } 376 | } 377 | 378 | stop_input_type( 379 | x, 380 | "an environment", 381 | ..., 382 | allow_na = FALSE, 383 | allow_null = allow_null, 384 | arg = arg, 385 | call = call 386 | ) 387 | } 388 | 389 | check_function <- function(x, 390 | ..., 391 | allow_null = FALSE, 392 | arg = caller_arg(x), 393 | call = caller_env()) { 394 | if (!missing(x)) { 395 | if (is_function(x)) { 396 | return(invisible(NULL)) 397 | } 398 | if (allow_null && is_null(x)) { 399 | return(invisible(NULL)) 400 | } 401 | } 402 | 403 | stop_input_type( 404 | x, 405 | "a function", 406 | ..., 407 | allow_na = FALSE, 408 | allow_null = allow_null, 409 | arg = arg, 410 | call = call 411 | ) 412 | } 413 | 414 | check_closure <- function(x, 415 | ..., 416 | allow_null = FALSE, 417 | arg = caller_arg(x), 418 | call = caller_env()) { 419 | if (!missing(x)) { 420 | if (is_closure(x)) { 421 | return(invisible(NULL)) 422 | } 423 | if (allow_null && is_null(x)) { 424 | return(invisible(NULL)) 425 | } 426 | } 427 | 428 | stop_input_type( 429 | x, 430 | "an R function", 431 | ..., 432 | allow_na = FALSE, 433 | allow_null = allow_null, 434 | arg = arg, 435 | call = call 436 | ) 437 | } 438 | 439 | check_formula <- function(x, 440 | ..., 441 | allow_null = FALSE, 442 | arg = caller_arg(x), 443 | call = caller_env()) { 444 | if (!missing(x)) { 445 | if (is_formula(x)) { 446 | return(invisible(NULL)) 447 | } 448 | if (allow_null && is_null(x)) { 449 | return(invisible(NULL)) 450 | } 451 | } 452 | 453 | stop_input_type( 454 | x, 455 | "a formula", 456 | ..., 457 | allow_na = FALSE, 458 | allow_null = allow_null, 459 | arg = arg, 460 | call = call 461 | ) 462 | } 463 | 464 | 465 | # Vectors ----------------------------------------------------------------- 466 | 467 | # TODO: Figure out what to do with logical `NA` and `allow_na = TRUE` 468 | 469 | check_character <- function(x, 470 | ..., 471 | allow_na = TRUE, 472 | allow_null = FALSE, 473 | arg = caller_arg(x), 474 | call = caller_env()) { 475 | 476 | if (!missing(x)) { 477 | if (is_character(x)) { 478 | if (!allow_na && any(is.na(x))) { 479 | abort( 480 | sprintf("`%s` can't contain NA values.", arg), 481 | arg = arg, 482 | call = call 483 | ) 484 | } 485 | 486 | return(invisible(NULL)) 487 | } 488 | 489 | if (allow_null && is_null(x)) { 490 | return(invisible(NULL)) 491 | } 492 | } 493 | 494 | stop_input_type( 495 | x, 496 | "a character vector", 497 | ..., 498 | allow_null = allow_null, 499 | arg = arg, 500 | call = call 501 | ) 502 | } 503 | 504 | check_logical <- function(x, 505 | ..., 506 | allow_null = FALSE, 507 | arg = caller_arg(x), 508 | call = caller_env()) { 509 | if (!missing(x)) { 510 | if (is_logical(x)) { 511 | return(invisible(NULL)) 512 | } 513 | if (allow_null && is_null(x)) { 514 | return(invisible(NULL)) 515 | } 516 | } 517 | 518 | stop_input_type( 519 | x, 520 | "a logical vector", 521 | ..., 522 | allow_na = FALSE, 523 | allow_null = allow_null, 524 | arg = arg, 525 | call = call 526 | ) 527 | } 528 | 529 | check_data_frame <- function(x, 530 | ..., 531 | allow_null = FALSE, 532 | arg = caller_arg(x), 533 | call = caller_env()) { 534 | if (!missing(x)) { 535 | if (is.data.frame(x)) { 536 | return(invisible(NULL)) 537 | } 538 | if (allow_null && is_null(x)) { 539 | return(invisible(NULL)) 540 | } 541 | } 542 | 543 | stop_input_type( 544 | x, 545 | "a data frame", 546 | ..., 547 | allow_null = allow_null, 548 | arg = arg, 549 | call = call 550 | ) 551 | } 552 | 553 | # nocov end 554 | -------------------------------------------------------------------------------- /R/promises.R: -------------------------------------------------------------------------------- 1 | #' @importFrom promises %...>% 2 | #' @export 3 | promises::"%...>%" 4 | 5 | #' @importFrom promises %...!% 6 | #' @export 7 | promises::"%...!%" 8 | 9 | #' @importFrom promises %...T>% 10 | #' @export 11 | promises::"%...T>%" 12 | 13 | #' @importFrom promises %...T!% 14 | #' @export 15 | promises::"%...T!%" 16 | 17 | #' @importFrom magrittr %>% 18 | #' @export 19 | magrittr::"%>%" 20 | 21 | #' @importFrom magrittr %T>% 22 | #' @export 23 | magrittr::"%T>%" 24 | 25 | #' @importFrom promises promise 26 | #' @export 27 | promises::promise 28 | 29 | #' @importFrom promises then 30 | #' @export 31 | promises::then 32 | 33 | #' @importFrom promises catch 34 | #' @export 35 | promises::catch 36 | 37 | #' @importFrom promises finally 38 | #' @export 39 | promises::finally 40 | 41 | promise_timeout <- function( 42 | p, 43 | timeout, 44 | loop = current_loop(), 45 | timeout_message = NULL 46 | ) { 47 | promise(function(resolve, reject) { 48 | cancel_timer <- later_with_interrupt( 49 | function() { 50 | if (is.null(timeout_message)) { 51 | timeout_message <- "Promise timed out" 52 | } 53 | 54 | reject(timeout_message) 55 | }, 56 | timeout, 57 | loop = loop, 58 | on_interrupt = function() { 59 | reject("interrupted") 60 | } 61 | ) 62 | 63 | p$then( 64 | onFulfilled = function(value) { 65 | # Timer is no longer needed, so we'll cancel it to free memory. 66 | cancel_timer() 67 | resolve(value) 68 | }, 69 | onRejected = function(err) { 70 | cancel_timer() 71 | reject(err) 72 | } 73 | ) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /R/protocol.R: -------------------------------------------------------------------------------- 1 | #' @import rlang 2 | 3 | utils::globalVariables( 4 | c("self", "private", "callback_", "error_", "timeout", "timeout_", "wait_") 5 | ) 6 | 7 | # Given a protocol spec (essentially, the Chrome DevTools Protocol JSON 8 | # converted to an R object), returns a list of domains of the DevTools 9 | # Protocol (like Browser, Page, Runtime). Each domain has a function for each 10 | # command and event (like Browser$getVersion, Page$navigate, etc). The 11 | # `protocol` input is the protocol object from the browser, translated from 12 | # JSON to an R object, and the `env` is the desired environment that is 13 | # assigned to the the generated functions -- it should be the Chromote 14 | # object's enclosing environment so that the functions can find `self` and 15 | # `private`. 16 | process_protocol <- function(protocol, env) { 17 | domains <- protocol$domains 18 | names(domains) <- vapply(domains, function(d) d$domain, "") 19 | 20 | domains <- lapply(domains, function(domain) { 21 | commands <- get_items(domain, "commands") 22 | commands <- lapply( 23 | commands, 24 | command_to_function, 25 | domain_name = domain$domain, 26 | env = env 27 | ) 28 | 29 | events <- get_items(domain, "events") 30 | events <- lapply( 31 | events, 32 | event_to_function, 33 | domain_name = domain$domain, 34 | env = env 35 | ) 36 | 37 | c(commands, events) 38 | }) 39 | 40 | domains 41 | } 42 | 43 | # Returns commands or events for a given domain 44 | get_items <- function(domain, type = c("commands", "events")) { 45 | type <- match.arg(type) 46 | methods <- domain[[type]] 47 | if (is.null(methods)) { 48 | return(list()) 49 | } else { 50 | names(methods) <- fetch_key_c(methods, "name") 51 | methods 52 | } 53 | } 54 | 55 | command_to_function <- function(command, domain_name, env) { 56 | new_function( 57 | args = gen_command_args(command$parameters), 58 | body = gen_command_body( 59 | paste0(domain_name, ".", command$name), 60 | command$parameters 61 | ), 62 | env = env 63 | ) 64 | # TODO: 65 | # * Add type-checking 66 | # * Cross-reference types for type checking 67 | } 68 | 69 | gen_command_args <- function(params) { 70 | args <- lapply(params, function(param) { 71 | if (!isTRUE(param$optional)) { 72 | missing_arg() 73 | } else { 74 | NULL 75 | } 76 | }) 77 | 78 | names(args) <- fetch_key_c(params, "name") 79 | args <- c( 80 | args, 81 | callback_ = list(NULL), 82 | error_ = list(NULL), 83 | timeout_ = if ("timeout" %in% names(args)) { 84 | expr(missing_arg()) 85 | } else { 86 | expr(self$default_timeout) 87 | }, 88 | wait_ = TRUE 89 | ) 90 | args 91 | } 92 | 93 | # Returns a function body for a command. 94 | # method_name is something like "Browser.getVersion" 95 | gen_command_body <- function(method_name, params) { 96 | # Construct expressions for checking missing args 97 | required_params <- params[!fetch_key_l(params, "optional", default = FALSE)] 98 | check_missing_exprs <- lapply(required_params, function(param) { 99 | name <- as.symbol(param$name) 100 | check_missing <- expr( 101 | if (missing(!!name)) 102 | stop("Missing required argument ", !!(expr_text(name))) 103 | ) 104 | }) 105 | 106 | timeout_default_expr <- 107 | if ("timeout" %in% lapply(params, `[[`, "name")) { 108 | # Set the wall time of chromote to twice that of the execution time. 109 | expr({ 110 | if (is_missing(timeout_)) { 111 | timeout_ <- 112 | if (is.null(timeout)) { 113 | self$default_timeout 114 | } else { 115 | 2 * timeout / 1000 116 | } 117 | } 118 | }) 119 | } else { 120 | expr({ 121 | }) 122 | } 123 | 124 | # As of 2025-02-07, it's not possible to query CDP to determine if the value 125 | # of `mobile` in the device metrics override, so we need to track its value 126 | # through any calls to `Emulation.setDeviceMetricsOverride`. 127 | track_device_override_mobile <- 128 | if (identical(method_name, "Emulation.setDeviceMetricsOverride")) { 129 | expr({ 130 | if (!!sym("deviceScaleFactor") > 0) { 131 | private$pixel_ratio <- !!sym("deviceScaleFactor") 132 | } else { 133 | private$pixel_ratio <- NULL 134 | } 135 | private$is_mobile <- !!sym("mobile") 136 | }) 137 | } else { 138 | expr({}) # fmt: skip 139 | } 140 | 141 | # Construct parameters for message 142 | param_list <- lapply(params, function(param) { 143 | as.symbol(param$name) 144 | }) 145 | names(param_list) <- fetch_key_c(params, "name") 146 | 147 | expr({ 148 | if (!is.null(callback_) && !is.function(callback_)) 149 | stop("`callback_` must be a function or NULL.") 150 | 151 | if (!is.null(error_) && !is.function(error_)) 152 | stop("`error_` must be a function or NULL.") 153 | 154 | !!!timeout_default_expr 155 | if (!is.null(timeout_) && !is.numeric(timeout_)) 156 | stop("`timeout_` must be a number or NULL.") 157 | 158 | if (!identical(wait_, TRUE) && !identical(wait_, FALSE)) 159 | stop("`wait_` must be TRUE or FALSE.") 160 | 161 | # Check for missing non-optional args 162 | !!!check_missing_exprs 163 | 164 | !!!track_device_override_mobile 165 | 166 | msg <- list( 167 | method = !!method_name, 168 | params = drop_nulls(list(!!!param_list)) 169 | ) 170 | p <- self$send_command( 171 | msg, 172 | callback = callback_, 173 | error = error_, 174 | timeout = timeout_ 175 | ) 176 | 177 | if (wait_) { 178 | self$wait_for(p) 179 | } else { 180 | p 181 | } 182 | }) 183 | } 184 | 185 | event_to_function <- function(event, domain_name, env) { 186 | new_function( 187 | args = list( 188 | callback_ = NULL, 189 | timeout_ = expr(self$default_timeout), 190 | wait_ = TRUE 191 | ), 192 | body = gen_event_body(paste0(domain_name, ".", event$name)), 193 | env = env 194 | ) 195 | } 196 | 197 | # Returns a function body for registering an event callback. 198 | # method_name is something like "Page.loadEventFired". 199 | gen_event_body <- function(method_name) { 200 | expr({ 201 | if (!is.null(callback_) && !is.function(callback_)) 202 | stop("`callback_` must be a function or NULL.") 203 | 204 | if (!is.null(timeout_) && !is.numeric(timeout_)) 205 | stop("`timeout_` must be a number or NULL.") 206 | 207 | if (!identical(wait_, TRUE) && !identical(wait_, FALSE)) 208 | stop("`wait_` must be TRUE or FALSE.") 209 | 210 | p <- private$register_event_listener(!!method_name, callback_, timeout_) 211 | 212 | # If callback_ was a function, then because the callback can fire multiple 213 | # times, p is not a promise; it is a function for deregistering the 214 | # callback. 215 | if (!is.null(callback_)) { 216 | return(invisible(p)) 217 | } 218 | 219 | if (wait_) { 220 | self$wait_for(p) 221 | } else { 222 | p 223 | } 224 | }) 225 | } 226 | 227 | # Given a protocol object, reassign the environment for all functions. 228 | protocol_reassign_envs <- function(protocol, env) { 229 | lapply(protocol, function(domain) { 230 | lapply(domain, function(method) { 231 | environment(method) <- env 232 | method 233 | }) 234 | }) 235 | } 236 | -------------------------------------------------------------------------------- /R/screenshot.R: -------------------------------------------------------------------------------- 1 | chromote_session_screenshot <- function( 2 | self, 3 | private, 4 | filename = "screenshot.png", 5 | selector = "html", 6 | cliprect = NULL, 7 | region = c("content", "padding", "border", "margin"), 8 | expand = NULL, 9 | scale = 1, 10 | show = FALSE, 11 | delay = 0.5, 12 | options = list(), 13 | wait_ = TRUE 14 | ) { 15 | force(filename) 16 | force(selector) 17 | force(cliprect) 18 | force(region) 19 | force(expand) 20 | force(scale) 21 | force(show) 22 | force(wait_) 23 | 24 | region = match.arg(region) 25 | if (length(filename) == 0 && !show) { 26 | stop("Cannot have empty filename and show=FALSE") 27 | } 28 | 29 | if (!is.null(cliprect) && !(is.numeric(cliprect) && length(cliprect) == 4)) { 30 | stop( 31 | "`cliprect` must be NULL or a numeric vector with 4 elements (for left, top, width, and height)." 32 | ) 33 | } 34 | 35 | if (is.null(expand)) { 36 | expand <- 0 37 | } 38 | if ( 39 | !is.numeric(expand) || 40 | !(length(expand) == 1 || length(expand) == 4) 41 | ) { 42 | stop( 43 | "`expand` must be NULL, or a numeric vector with 1 or 4 elements (for top, right, bottom, left)" 44 | ) 45 | } 46 | if (length(expand) == 1) { 47 | expand <- rep(expand, 4) 48 | } 49 | 50 | stopifnot( 51 | "`options` must be a list" = rlang::is_list(options), 52 | "`options` must be named" = rlang::is_named2(options) 53 | ) 54 | # Set up arg list from defaults & user options to pass to `Page$captureScreenshot` 55 | screenshot_arg_defaults <- list( 56 | fromSurface = TRUE, 57 | captureBeyondViewport = TRUE 58 | ) 59 | screenshot_args <- utils::modifyList(screenshot_arg_defaults, options) 60 | if (is.null(screenshot_args$format)) { 61 | screenshot_args$format <- screenshot_format(filename) 62 | } 63 | 64 | # These vars are used to store information gathered from one step to use 65 | # in a later step. 66 | image_data <- NULL 67 | overall_width <- NULL 68 | overall_height <- NULL 69 | root_node_id <- NULL 70 | pixel_ratio <- NULL 71 | 72 | # Setup stuff for both selector and cliprect code paths. 73 | p <- self$Emulation$setScrollbarsHidden( 74 | hidden = TRUE, 75 | wait_ = FALSE 76 | )$then(function(value) { 77 | # Get device pixel ratio if unknown 78 | private$get_pixel_ratio() 79 | })$then(function(value) { 80 | pixel_ratio <<- value 81 | })$then(function(value) { 82 | # Get overall height and width of the root node 83 | self$DOM$getDocument(wait_ = FALSE) 84 | })$then(function(value) { 85 | root_node_id <<- value$root$nodeId 86 | self$DOM$querySelector(value$root$nodeId, "html", wait_ = FALSE) 87 | })$then(function(value) { 88 | self$DOM$getBoxModel(value$nodeId, wait_ = FALSE) 89 | })$then(function(value) { 90 | overall_width <<- value$model$width 91 | overall_height <<- value$model$height 92 | 93 | promise(function(resolve, reject) { 94 | # Wait `delay` seconds for resize to complete. For complicated apps this may need to be longer. 95 | ## TODO: Can we wait for an event instead? 96 | later(function() resolve(TRUE), delay) 97 | }) 98 | }) 99 | 100 | if (is.null(cliprect)) { 101 | # This code path uses the selector instead of cliprect. 102 | p <- p$then(function(value) { 103 | find_selectors_bounds(self, root_node_id, selector, region) 104 | })$then(function(value) { 105 | # Note: `expand` values are top, right, bottom, left. 106 | xmin <- value$xmin - expand[4] 107 | xmax <- value$xmax + expand[2] 108 | ymin <- value$ymin - expand[1] 109 | ymax <- value$ymax + expand[3] 110 | 111 | # We need to make sure that we don't go beyond the bounds of the 112 | # page. 113 | xmin <- max(xmin, 0) 114 | xmax <- min(xmax, overall_width) 115 | ymin <- max(ymin, 0) 116 | ymax <- min(ymax, overall_height) 117 | 118 | screenshot_args$clip <- list( 119 | x = xmin, 120 | y = ymin, 121 | width = xmax - xmin, 122 | height = ymax - ymin, 123 | scale = scale / pixel_ratio 124 | ) 125 | screenshot_args$wait_ <- FALSE 126 | 127 | do.call(self$Page$captureScreenshot, screenshot_args) 128 | })$then(function(value) { 129 | image_data <<- value 130 | }) 131 | } else { 132 | # If cliprect was provided, use it instead of selector 133 | p <- p$then(function(value) { 134 | screenshot_args$clip <- list( 135 | x = cliprect[[1]], 136 | y = cliprect[[2]], 137 | width = cliprect[[3]], 138 | height = cliprect[[4]], 139 | scale = scale / pixel_ratio 140 | ) 141 | screenshot_args$wait_ <- FALSE 142 | 143 | do.call(self$Page$captureScreenshot, screenshot_args) 144 | })$then(function(value) { 145 | image_data <<- value 146 | }) 147 | } 148 | 149 | p <- p$then(function(value) { 150 | # Un-hide scrollbars 151 | self$Emulation$setScrollbarsHidden(hidden = FALSE, wait_ = FALSE) 152 | })$then(function(value) { 153 | temp_output <- FALSE 154 | if (is.null(filename)) { 155 | temp_output <- TRUE 156 | filename <- tempfile("chromote-screenshot-", fileext = ".png") 157 | on.exit(unlink(filename)) 158 | } 159 | 160 | writeBin(jsonlite::base64_dec(image_data$data), filename) 161 | if (show) { 162 | showimage::show_image(filename) 163 | } 164 | 165 | if (temp_output) { 166 | invisible() 167 | } else { 168 | invisible(filename) 169 | } 170 | })$catch(function(err) { 171 | warning("An error occurred: ", err) 172 | }) 173 | 174 | if (wait_) { 175 | self$wait_for(p) 176 | } else { 177 | p 178 | } 179 | } 180 | 181 | screenshot_format <- function(filename) { 182 | ext <- strsplit(filename, ".", fixed = TRUE)[[1]] 183 | if (length(ext) < 2) ext <- "no_ext" 184 | ext <- ext[length(ext)] 185 | 186 | switch( 187 | tolower(ext), 188 | png = "png", 189 | jpg = , 190 | jpeg = "jpeg", 191 | webp = "webp", 192 | pdf = rlang::abort( 193 | "Use the `screenshot_pdf()` method to capture a PDF screenshot." 194 | ), 195 | no_ext = rlang::abort( 196 | sprintf( 197 | 'Could not guess screenshot format from filename "%s". Does the name include a file extension?', 198 | filename 199 | ) 200 | ), 201 | rlang::abort( 202 | sprintf('"%s" is not a supported screenshot format.', ext) 203 | ) 204 | ) 205 | } 206 | 207 | chromote_session_screenshot_pdf <- function( 208 | self, 209 | private, 210 | filename = "screenshot.pdf", 211 | pagesize = "letter", 212 | margins = 0.5, 213 | units = c("in", "cm"), 214 | landscape = FALSE, 215 | display_header_footer = FALSE, 216 | print_background = FALSE, 217 | scale = 1, 218 | wait_ = TRUE 219 | ) { 220 | force(filename) 221 | force(pagesize) 222 | force(margins) 223 | force(units) 224 | force(landscape) 225 | force(display_header_footer) 226 | force(print_background) 227 | force(scale) 228 | force(wait_) 229 | 230 | page_sizes <- list( 231 | letter = c(8.5, 11), 232 | legal = c(8.5, 14), 233 | tabloid = c(11, 17), 234 | ledger = c(17, 11), 235 | a0 = c(33.1, 46.8), 236 | a1 = c(23.4, 33.1), 237 | a2 = c(16.54, 23.4), 238 | a3 = c(11.7, 16.54), 239 | a4 = c(8.27, 11.7), 240 | a5 = c(5.83, 8.27), 241 | a6 = c(4.13, 5.83) 242 | ) 243 | 244 | units <- match.arg(units) 245 | 246 | if (units == "cm") { 247 | margins <- margins / 2.54 248 | } 249 | 250 | if (is.character(pagesize)) { 251 | pagesize <- tolower(pagesize) 252 | pagesize <- match.arg(pagesize, names(page_sizes)) 253 | pagesize <- page_sizes[[pagesize]] 254 | } else if (is.numeric(pagesize) && length(pagesize) == 2) { 255 | # User has passed in width and height values 256 | if (units == "cm") { 257 | pagesize <- pagesize / 2.54 258 | } 259 | } else { 260 | stop( 261 | '`pagesize` must be one of "', 262 | paste(names(page_sizes), collapse = '", "'), 263 | '", or a two-element vector of width and height.' 264 | ) 265 | } 266 | 267 | if (length(margins) == 1) { 268 | margins <- rep(margins, 4) 269 | } 270 | if (length(margins) != 4) { 271 | stop( 272 | '`margins` must be a single number, or a four-element numeric vector representing', 273 | ' the margins for top, right, bottom, and left, respectively.' 274 | ) 275 | } 276 | 277 | p <- self$Page$printToPDF( 278 | landscape = landscape, 279 | displayHeaderFooter = display_header_footer, 280 | printBackground = print_background, 281 | scale = scale, 282 | paperWidth = pagesize[[1]], 283 | paperHeight = pagesize[[2]], 284 | marginTop = margins[[1]], 285 | marginBottom = margins[[3]], 286 | marginLeft = margins[[4]], 287 | marginRight = margins[[2]], 288 | wait_ = FALSE 289 | )$then(function(value) { 290 | writeBin(jsonlite::base64_dec(value$data), filename) 291 | filename 292 | }) 293 | 294 | if (wait_) { 295 | invisible(self$wait_for(p)) 296 | } else { 297 | p 298 | } 299 | } 300 | 301 | # Find a bounding box that contains the elements selected by any number of 302 | # selectors. Note that a selector can pick out more than one element. 303 | find_selectors_bounds <- function( 304 | cm, 305 | root_node_id, 306 | selectors, 307 | region = "content" 308 | ) { 309 | ps <- lapply(selectors, function(selector) { 310 | cm$DOM$querySelectorAll(root_node_id, selector, wait_ = FALSE)$then( 311 | function(value) { 312 | # There can be multiple nodes for a given selector, so we need to 313 | # process all of them. 314 | ps <- lapply(value$nodeIds, function(nodeId) { 315 | cm$DOM$getBoxModel(nodeId, wait_ = FALSE)$catch(function(value) { 316 | # Can get an error, "Could not compute box model", if the element 317 | # is not visible. Just return NULL in this case. 318 | NULL 319 | }) 320 | }) 321 | 322 | promise_all(.list = ps) 323 | } 324 | )$then(function(values) { 325 | # Could have gotten emtpy list for non-visible elements; remove them. 326 | values <- drop_nulls(values) 327 | 328 | lapply(values, function(value) { 329 | list( 330 | xmin = value$model[[region]][[1]], 331 | xmax = value$model[[region]][[3]], 332 | ymin = value$model[[region]][[2]], 333 | ymax = value$model[[region]][[6]] 334 | ) 335 | }) 336 | }) 337 | }) 338 | 339 | promise_all(.list = ps)$then(function(value) { 340 | value <- unlist(value, recursive = FALSE) 341 | if (length(value) == 0) { 342 | stop("Unable to find any visible elements for selectors.") 343 | } 344 | 345 | list( 346 | xmin = min(fetch_key_n(value, "xmin")), 347 | xmax = max(fetch_key_n(value, "xmax")), 348 | ymin = min(fetch_key_n(value, "ymin")), 349 | ymax = max(fetch_key_n(value, "ymax")) 350 | ) 351 | }) 352 | } 353 | -------------------------------------------------------------------------------- /R/synchronize.R: -------------------------------------------------------------------------------- 1 | promise_globals <- new.env(parent = emptyenv()) 2 | promise_globals$interrupt_domains <- list() 3 | 4 | push_interrupt_domain <- function(domain) { 5 | n_domains <- length(promise_globals$interrupt_domains) 6 | promise_globals$interrupt_domains[[n_domains + 1]] <- domain 7 | } 8 | 9 | pop_interrupt_domain <- function() { 10 | n_domains <- length(promise_globals$interrupt_domains) 11 | if (length(n_domains) == 0) return(NULL) 12 | 13 | domain <- promise_globals$interrupt_domains[[n_domains]] 14 | promise_globals$interrupt_domains[[n_domains]] <- NULL 15 | 16 | domain 17 | } 18 | 19 | current_interrupt_domain <- function() { 20 | if (length(promise_globals$interrupt_domains) == 0) { 21 | return(NULL) 22 | } 23 | 24 | promise_globals$interrupt_domains[[length(promise_globals$interrupt_domains)]] 25 | } 26 | 27 | create_interrupt_domain <- function() { 28 | domain <- new_promise_domain( 29 | wrapOnFulfilled = function(onFulfilled) { 30 | function(...) { 31 | push_interrupt_domain(domain) 32 | on.exit(pop_interrupt_domain(), add = TRUE) 33 | 34 | if (domain$interrupted) { 35 | stop("Operation was interrupted 1") 36 | } 37 | tryCatch( 38 | { 39 | onFulfilled(...) 40 | }, 41 | interrupt = function(e) { 42 | # message("wrapOnFulfilled inner caught interrupt") 43 | # Call function here that returns current interrupt 44 | domain$interrupted <- TRUE 45 | stop("Operation was interrupted 2") 46 | } 47 | ) 48 | } 49 | }, 50 | wrapOnRejected = function(onRejected) { 51 | function(...) { 52 | push_interrupt_domain(domain) 53 | on.exit(pop_interrupt_domain(), add = TRUE) 54 | 55 | if (domain$interrupted) { 56 | stop("Operation was interrupted 3") 57 | } 58 | tryCatch( 59 | onRejected(...), 60 | interrupt = function(e) { 61 | domain$interrupted <- TRUE 62 | stop("Operation was interrupted 4") 63 | } 64 | ) 65 | } 66 | }, 67 | wrapOnFinally = function(onFinally) { 68 | function(...) { 69 | push_interrupt_domain(domain) 70 | on.exit(pop_interrupt_domain(), add = TRUE) 71 | 72 | tryCatch( 73 | onFinally(...), 74 | interrupt = function(e) { 75 | domain$interrupted <- TRUE 76 | stop("Operation was interrupted 5") 77 | } 78 | ) 79 | } 80 | }, 81 | wrapSync = function(expr) { 82 | push_interrupt_domain(domain) 83 | on.exit(pop_interrupt_domain(), add = TRUE) 84 | 85 | # Counting is currently not used 86 | if (is.null(promise_globals$synchronized)) { 87 | promise_globals$synchronized <- 0L 88 | } 89 | promise_globals$synchronized <- promise_globals$synchronized + 1L 90 | on.exit( 91 | promise_globals$synchronized <- promise_globals$synchronized - 1L, 92 | add = TRUE 93 | ) 94 | 95 | force(expr) 96 | }, 97 | interrupted = FALSE 98 | ) 99 | 100 | domain 101 | } 102 | 103 | # This function takes a promise and blocks until it is resolved. It runs the 104 | # promise's callbacks in the provided event loop. If the promise is 105 | # interrupted then this function tries to catch the interrupt, then runs the 106 | # loop until it's empty; then it throws a new interrupt. If the promise throws 107 | # an error, then also throws an error. 108 | synchronize <- function(expr, loop) { 109 | domain <- create_interrupt_domain() 110 | 111 | with_promise_domain(domain, { 112 | tryCatch( 113 | { 114 | result <- force(expr) 115 | 116 | if (is.promising(result)) { 117 | value <- NULL 118 | type <- NULL 119 | result$then(function(val) { 120 | value <<- val 121 | type <<- "success" 122 | })$catch(function(reason) { 123 | value <<- reason 124 | type <<- "error" 125 | }) 126 | 127 | while (is.null(type) && !domain$interrupted) { 128 | run_now(loop = loop) 129 | } 130 | 131 | if (is.null(type)) { 132 | generateInterrupt() 133 | } else if (type == "success") { 134 | value 135 | } else if (type == "error") { 136 | stop(value) 137 | } 138 | } 139 | }, 140 | interrupt = function(e) { 141 | domain$interrupted <<- TRUE 142 | message( 143 | "Attempting to interrupt gracefully; press Esc/Ctrl+C to force interrupt" 144 | ) 145 | while (!loop_empty(loop = loop)) { 146 | run_now(loop = loop) 147 | } 148 | generateInterrupt() 149 | } 150 | ) 151 | }) 152 | } 153 | 154 | # A wrapper for later() which polls for interrupts. If an interrupt has 155 | # occurred either while running another callback, or when run_now() is 156 | # waiting, the interrupt will be detected and (1) the scheduled `func` will be 157 | # cancelled, and (2) the `on_interrupt` callback will be invoked. 158 | later_with_interrupt <- function( 159 | func, 160 | delay = 0, 161 | loop = current_loop(), 162 | on_interrupt = function() { 163 | }, 164 | interrupt_domain = current_interrupt_domain(), 165 | poll_interval = 0.1 166 | ) { 167 | force(func) 168 | force(loop) 169 | force(interrupt_domain) 170 | force(on_interrupt) 171 | force(poll_interval) 172 | 173 | if (is.null(interrupt_domain)) { 174 | return(later(func, delay, loop)) 175 | } 176 | 177 | end_time <- as.numeric(Sys.time()) + delay 178 | cancel <- NULL 179 | 180 | nextTurn <- function(init = FALSE) { 181 | if (isTRUE(interrupt_domain$interrupted)) { 182 | on_interrupt() 183 | return() 184 | } 185 | 186 | this_delay <- min(poll_interval, end_time - as.numeric(Sys.time())) 187 | if (this_delay <= 0) { 188 | # Time has expired. If we've never progressed to the next tick (i.e. 189 | # init == TRUE) then don't invoke the callback yet, wait until the next 190 | # tick. Otherwise, do invoke the callback. 191 | if (!init) { 192 | func() 193 | return() 194 | } 195 | this_delay <- 0 196 | } 197 | cancel <<- later::later(nextTurn, this_delay, loop) 198 | } 199 | nextTurn(init = TRUE) 200 | 201 | function() { 202 | cancel() 203 | } 204 | } 205 | # TODO 206 | # 207 | # The notion of cancellability should go into later package. Instead of this 208 | # function taking `interrupt_domain`, which is a promise-level object, we could 209 | # do something like the following: 210 | # 211 | # Add on_interrupt option to later(); if FALSE/NULL (the default) then interrupts 212 | # don't affect scheduled callback. If TRUE, then interrupt cancels the later(). 213 | # If a function, then interrupt cancels the later() and calls the on_interrupt 214 | # function. 215 | # Add later::interrupt() function, so that later can know that an interrupt 216 | # happened. 217 | # Add option to later() to make the callback uninterruptable. 218 | 219 | generateInterrupt <- function() { 220 | tools::pskill(Sys.getpid(), tools::SIGINT) 221 | Sys.sleep(1) 222 | } 223 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | cat_line <- function(...) { 2 | cat(paste0(..., "\n", collapse = "")) 3 | } 4 | 5 | # ============================================================================= 6 | # System 7 | # ============================================================================= 8 | 9 | is_windows <- function() .Platform$OS.type == "windows" 10 | 11 | is_mac <- function() Sys.info()[['sysname']] == 'Darwin' 12 | 13 | is_linux <- function() Sys.info()[['sysname']] == 'Linux' 14 | 15 | is_openbsd <- function() Sys.info()[['sysname']] == "OpenBSD" 16 | 17 | # ============================================================================= 18 | # Vectors 19 | # ============================================================================= 20 | 21 | get_key <- function(x, key, default = stop("Key not present")) { 22 | if (key %in% names(x)) { 23 | x[[key]] 24 | } else { 25 | default 26 | } 27 | } 28 | 29 | fetch_key_list <- function(x, key, default = stop("Key not present")) { 30 | lapply(x, get_key, key, default = default) 31 | } 32 | 33 | fetch_key_c <- function(x, key, default = stop("Key not present")) { 34 | vapply(x, get_key, key, default = default, FUN.VALUE = "") 35 | } 36 | 37 | fetch_key_n <- function(x, key, default = stop("Key not present")) { 38 | vapply(x, get_key, key, default = default, FUN.VALUE = 0.0) 39 | } 40 | 41 | fetch_key_i <- function(x, key, default = stop("Key not present")) { 42 | vapply(x, get_key, key, default = default, FUN.VALUE = 0L) 43 | } 44 | 45 | fetch_key_l <- function(x, key, default = stop("Key not present")) { 46 | vapply(x, get_key, key, default = default, FUN.VALUE = FALSE) 47 | } 48 | 49 | drop_nulls <- function(x) { 50 | x[!vapply(x, is.null, TRUE)] 51 | } 52 | 53 | # ============================================================================= 54 | # Text 55 | # ============================================================================= 56 | 57 | truncate <- function(x, n = 1000, message = "[truncated]") { 58 | if (length(x) != 1) { 59 | stop("Input must be a single string") 60 | } 61 | if (nchar(x) > n) { 62 | x <- paste0(substr(x, 1, n - nchar(message)), message) 63 | } 64 | x 65 | } 66 | 67 | # ============================================================================= 68 | # Protocol-related stuff 69 | # ============================================================================= 70 | 71 | # Given an event name, return the domain: "Page.loadEventFired" -> "Page" 72 | find_domain <- function(event) { 73 | sub("\\.[^.]+", "", event) 74 | } 75 | 76 | # ============================================================================= 77 | # Browser 78 | # ============================================================================= 79 | 80 | # Force url to be opened by Chromium browser 81 | browse_url <- function(path, chromote) { 82 | if (grepl("^[a-zA-Z][a-zA-Z0-9+.-]*://", path)) { 83 | # `path` is already a full URL 84 | url <- path 85 | } else { 86 | url <- chromote$url(path) 87 | } 88 | 89 | browser <- chromote$get_browser() 90 | if (inherits(browser, "Chrome")) { 91 | # If locally available, use the local browser 92 | browser_path <- browser$get_path() 93 | product <- chromote$Browser$getVersion(wait_ = TRUE)$product 94 | 95 | # And if not chrome-headless-shell (which doesn't have a UI we can use) 96 | if (grepl("HeadlessChrome", product, fixed = TRUE)) { 97 | cli::cli_warn( 98 | "Cannot open a browser window with {.field chrome-headless-shell}, using your default browser instead." 99 | ) 100 | } else { 101 | # Quote the path if using a non-windows machine 102 | if (!is_windows()) browser_path <- shQuote(browser_path) 103 | utils::browseURL(url, browser_path) 104 | return(invisible(url)) 105 | } 106 | } 107 | 108 | # Otherwise pray opening the url works as expected 109 | # Users can set `options(browser=)` to override default behavior 110 | utils::browseURL(url) 111 | invisible(url) 112 | } 113 | 114 | # ============================================================================= 115 | # Random Ports 116 | # ============================================================================= 117 | # 118 | # Borrowed from https://github.com/rstudio/httpuv/blob/main/R/random_port.R 119 | 120 | #' Startup a service that requires a random port 121 | #' 122 | #' `with_random_port()` provides `startup()` with a random port value and runs 123 | #' the function: 124 | #' 125 | #' 1. `startup()` always returns a value if successful. 126 | #' 2. If `startup()` fails with a generic error, we assume the port is occupied 127 | #' and try the next random port. 128 | #' 3. If `startup()` fails with an error classed with errors in `stop_on`, 129 | #' (`error_stop_port_search` or `system_command_error` by default), we stop 130 | #' the port search and rethrow the fatal error. 131 | #' 4. If we try `n` random ports, the port search stops with an informative 132 | #' error that includes the last port attempt error. 133 | #' 134 | #' @param startup A function that takes a `port` argument, where `port` will be 135 | #' randomly selected. When successful, `startup()` should return a non-NULL 136 | #' value that will also be returned from `with_random_port()`. Generic errors 137 | #' emitted by this function are silently ignored: when `startup()` fails, we 138 | #' assume the port was unavailable and we try with a new port. Errors with the 139 | #' classes in `stop_on` fail immediately. 140 | #' @param ... Additional arguments passed to `startup()`. 141 | #' @param min,max Port range 142 | #' @param n Maximum number of ports to try 143 | #' @param stop_on Error classes that signal the port search should be stopped 144 | #' 145 | #' @return The result of `startup()`, or an error if `startup()` fails. 146 | #' @noRd 147 | with_random_port <- function( 148 | startup, 149 | ..., 150 | min = 1024L, 151 | max = 49151L, 152 | n = 10, 153 | stop_on = c("error_stop_port_search", "system_command_error") 154 | ) { 155 | stopifnot(is.function(startup)) 156 | valid_ports <- setdiff(seq.int(min, max), unsafe_ports) 157 | 158 | # Try up to n ports 159 | n <- min(n, length(valid_ports)) 160 | ports <- sample(valid_ports, n) 161 | err_port <- NULL 162 | 163 | for (port in ports) { 164 | success <- FALSE 165 | res <- NULL 166 | err_fatal <- NULL 167 | 168 | # Try to run `startup` with the random port 169 | tryCatch( 170 | { 171 | res <- startup(port = port, ...) 172 | success <- TRUE 173 | }, 174 | error = function(cnd) { 175 | if (rlang::cnd_inherits(cnd, stop_on)) { 176 | # Non generic errors that signal we should stop trying new ports 177 | err_fatal <<- cnd 178 | return() 179 | } 180 | # For other errors, they are probably because the port is already in 181 | # use. Don't do anything; we'll just continue in the loop, but we save 182 | # the last port retry error to throw in case it's informative. 183 | err_port <<- cnd 184 | NULL 185 | } 186 | ) 187 | 188 | if (!is.null(err_fatal)) { 189 | rlang::cnd_signal(err_fatal) 190 | } 191 | 192 | if (isTRUE(success)) { 193 | return(res) 194 | } 195 | } 196 | 197 | rlang::abort( 198 | "Cannot find an available port. Please try again.", 199 | class = "error_no_available_port", 200 | parent = err_port 201 | ) 202 | } 203 | 204 | # Ports that are considered unsafe by Chrome 205 | # http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome 206 | # https://github.com/rstudio/shiny/issues/1784 207 | unsafe_ports <- c( 208 | 1, 209 | 7, 210 | 9, 211 | 11, 212 | 13, 213 | 15, 214 | 17, 215 | 19, 216 | 20, 217 | 21, 218 | 22, 219 | 23, 220 | 25, 221 | 37, 222 | 42, 223 | 43, 224 | 53, 225 | 77, 226 | 79, 227 | 87, 228 | 95, 229 | 101, 230 | 102, 231 | 103, 232 | 104, 233 | 109, 234 | 110, 235 | 111, 236 | 113, 237 | 115, 238 | 117, 239 | 119, 240 | 123, 241 | 135, 242 | 139, 243 | 143, 244 | 179, 245 | 389, 246 | 427, 247 | 465, 248 | 512, 249 | 513, 250 | 514, 251 | 515, 252 | 526, 253 | 530, 254 | 531, 255 | 532, 256 | 540, 257 | 548, 258 | 556, 259 | 563, 260 | 587, 261 | 601, 262 | 636, 263 | 993, 264 | 995, 265 | 2049, 266 | 3659, 267 | 4045, 268 | 6000, 269 | 6665, 270 | 6666, 271 | 6667, 272 | 6668, 273 | 6669, 274 | 6697 275 | ) 276 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | `_dummy_` <- function() { 2 | # Make a dummy curl call to make R CMD check happy 3 | # {jsonlite} only suggests {curl}, but is needed for standard {chromote} usage 4 | # https://github.com/rstudio/chromote/issues/37 5 | curl::curl 6 | 7 | invisible() 8 | } 9 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>", 15 | fig.path = "man/figures/README-", 16 | out.width = "100%" 17 | ) 18 | ``` 19 | 20 | # chromote chromote website 21 | 22 | 23 | [![R-CMD-check](https://github.com/rstudio/chromote/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/rstudio/chromote/actions) 24 | [![CRAN status](https://www.r-pkg.org/badges/version/chromote)](https://CRAN.R-project.org/package=chromote) 25 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 26 | 27 | 28 | ```{r child="man/fragments/features.Rmd"} 29 | ``` 30 | 31 | ## Learn More 32 | 33 | Learn more about using and programming with chromote: 34 | 35 | * [Get started](https://rstudio.github.io/chromote/articles/chromote.html) 36 | * [Commands and events](https://rstudio.github.io/chromote/articles/commands-and-events.html) 37 | * [Synchronous vs. asynchronous usage](https://rstudio.github.io/chromote/articles/sync-async.html) 38 | * [Choosing which Chrome-based browser to use](https://rstudio.github.io/chromote/articles/which-chrome.html) 39 | 40 | ```{r child="man/fragments/install.Rmd"} 41 | ``` 42 | 43 | ```{r child="man/fragments/basic-usage.Rmd"} 44 | ``` 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | # chromote chromote website 9 | 10 | 11 | 12 | [![R-CMD-check](https://github.com/rstudio/chromote/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/rstudio/chromote/actions) 13 | [![CRAN 14 | status](https://www.r-pkg.org/badges/version/chromote)](https://CRAN.R-project.org/package=chromote) 15 | [![Lifecycle: 16 | experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 17 | 18 | 19 | Chromote is an R implementation of the [Chrome DevTools 20 | Protocol](https://chromedevtools.github.io/devtools-protocol/). It works 21 | with Chrome, Chromium, Opera, Vivaldi, and other browsers based on 22 | [Chromium](https://www.chromium.org/). By default it uses Google Chrome 23 | (which must already be installed on the system). To use a different 24 | browser, see `vignette("which-chrome")`. 25 | 26 | Chromote is not the only R package that implements the Chrome DevTools 27 | Protocol. Here are some others: 28 | 29 | - [crrri](https://github.com/RLesur/crrri) by Romain Lesur and 30 | Christophe Dervieux 31 | - [decapitated](https://github.com/hrbrmstr/decapitated/) by Bob Rudis 32 | - [chradle](https://github.com/milesmcbain/chradle) by Miles McBain 33 | 34 | The interface to Chromote is similar to 35 | [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) 36 | for node.js. 37 | 38 | ## Features 39 | 40 | - Install and use specific versions of Chrome from the [Chrome for 41 | Testing](https://googlechromelabs.github.io/chrome-for-testing/) 42 | service. 43 | 44 | - Offers a synchronous API for ease of use and an asynchronous API for 45 | more sophisticated tasks. 46 | 47 | - Full support for the Chrome DevTools Protocol for any version of 48 | Chrome or any Chrome-based browser. 49 | 50 | - Includes convenience methods, like `$screenshot()` and 51 | `$set_viewport_size()`, for common tasks. 52 | 53 | - Automatically reconnects to previous sessions if the connection from R 54 | to Chrome is lost, for example when restarting from sleep state. 55 | 56 | - Powers many higher-level packages and functions, like `{shinytest2}` 57 | and `rvest::read_html_live()`. 58 | 59 | ## Learn More 60 | 61 | Learn more about using and programming with chromote: 62 | 63 | - [Get 64 | started](https://rstudio.github.io/chromote/articles/chromote.html) 65 | - [Commands and 66 | events](https://rstudio.github.io/chromote/articles/commands-and-events.html) 67 | - [Synchronous vs. asynchronous 68 | usage](https://rstudio.github.io/chromote/articles/sync-async.html) 69 | - [Choosing which Chrome-based browser to 70 | use](https://rstudio.github.io/chromote/articles/which-chrome.html) 71 | 72 | ## Installation 73 | 74 | Install the released version of chromote from CRAN: 75 | 76 | ``` r 77 | install.packages("chromote") 78 | ``` 79 | 80 | Or install the development version from GitHub with: 81 | 82 | ``` r 83 | # install.packages("pak") 84 | pak::pak("rstudio/chromote") 85 | ``` 86 | 87 | ## Basic usage 88 | 89 | This will start a headless browser and open an interactive viewer for it 90 | in a normal browser, so that you can see what the headless browser is 91 | doing. 92 | 93 | ``` r 94 | library(chromote) 95 | 96 | b <- ChromoteSession$new() 97 | 98 | # In a web browser, open a viewer for the headless browser. Works best with 99 | # Chromium-based browsers. 100 | b$view() 101 | ``` 102 | 103 | The browser can be given *commands*, as specified by the [Chrome 104 | DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). 105 | For example, `$Browser$getVersion()` (which corresponds to the 106 | [Browser.getVersion](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-getVersion) 107 | in the API docs) will query the browser for version information: 108 | 109 | ``` r 110 | b$Browser$getVersion() 111 | #> $protocolVersion 112 | #> [1] "1.3" 113 | #> 114 | #> $product 115 | #> [1] "HeadlessChrome/98.0.4758.102" 116 | #> 117 | #> $revision 118 | #> [1] "@273bf7ac8c909cde36982d27f66f3c70846a3718" 119 | #> 120 | #> $userAgent 121 | #> [1] "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/98.0.4758.102 Safari/537.36" 122 | #> 123 | #> $jsVersion 124 | #> [1] "9.8.177.11" 125 | ``` 126 | 127 | If you have the viewer open and run the following, you’ll see the web 128 | page load in the viewer[^1]: 129 | 130 | ``` r 131 | b$go_to("https://www.r-project.org/") 132 | ``` 133 | 134 | In addition to full support of the Chrome Devtools Protocol, 135 | `ChromoteSession` objects also have some convenience methods, like 136 | `$go_to()` and `$screenshot()`. (See the Examples section below for more 137 | information about screenshots.) 138 | 139 | ``` r 140 | # Saves to screenshot.png 141 | b$screenshot() 142 | 143 | # Takes a screenshot of elements picked out by CSS selector 144 | b$screenshot("sidebar.png", selector = ".sidebar") 145 | ``` 146 | 147 |
148 | A screenshot of the sidebar of r-rproject.org, circa 2023. 150 | 152 |
153 | 154 | [^1]: This simple example works interactively, but if you’re using 155 | chromote to programmatically take screenshots you’ll want to read 156 | `vignette("example-loading-page")` for a consistent and reliable 157 | approach. 158 | -------------------------------------------------------------------------------- /chromote.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## R CMD check results 2 | 3 | 0 errors | 0 warnings | 0 notes 4 | 5 | 6 | ## revdepcheck results 7 | 8 | We checked 25 reverse dependencies (24 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. 9 | 10 | * We saw 0 new problems 11 | * We failed to check 0 packages 12 | 13 | -------------------------------------------------------------------------------- /man/Browser.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/browser.R 3 | \name{Browser} 4 | \alias{Browser} 5 | \title{Browser base class} 6 | \description{ 7 | Base class for browsers like Chrome, Chromium, etc. Defines the interface 8 | used by various browser implementations. It can represent a local browser 9 | process or one running remotely. 10 | } 11 | \details{ 12 | The \code{initialize()} method of an implementation should set \code{private$host} 13 | and \code{private$port}. If the process is local, the \code{initialize()} method 14 | should also set \code{private$process}. 15 | } 16 | \section{Methods}{ 17 | \subsection{Public methods}{ 18 | \itemize{ 19 | \item \href{#method-Browser-is_local}{\code{Browser$is_local()}} 20 | \item \href{#method-Browser-get_process}{\code{Browser$get_process()}} 21 | \item \href{#method-Browser-is_alive}{\code{Browser$is_alive()}} 22 | \item \href{#method-Browser-get_host}{\code{Browser$get_host()}} 23 | \item \href{#method-Browser-get_port}{\code{Browser$get_port()}} 24 | \item \href{#method-Browser-close}{\code{Browser$close()}} 25 | \item \href{#method-Browser-clone}{\code{Browser$clone()}} 26 | } 27 | } 28 | \if{html}{\out{
}} 29 | \if{html}{\out{}} 30 | \if{latex}{\out{\hypertarget{method-Browser-is_local}{}}} 31 | \subsection{Method \code{is_local()}}{ 32 | Is local browser? 33 | Returns TRUE if the browser is running locally, FALSE if it's remote. 34 | \subsection{Usage}{ 35 | \if{html}{\out{
}}\preformatted{Browser$is_local()}\if{html}{\out{
}} 36 | } 37 | 38 | } 39 | \if{html}{\out{
}} 40 | \if{html}{\out{}} 41 | \if{latex}{\out{\hypertarget{method-Browser-get_process}{}}} 42 | \subsection{Method \code{get_process()}}{ 43 | Browser process 44 | \subsection{Usage}{ 45 | \if{html}{\out{
}}\preformatted{Browser$get_process()}\if{html}{\out{
}} 46 | } 47 | 48 | } 49 | \if{html}{\out{
}} 50 | \if{html}{\out{}} 51 | \if{latex}{\out{\hypertarget{method-Browser-is_alive}{}}} 52 | \subsection{Method \code{is_alive()}}{ 53 | Is the process alive? 54 | \subsection{Usage}{ 55 | \if{html}{\out{
}}\preformatted{Browser$is_alive()}\if{html}{\out{
}} 56 | } 57 | 58 | } 59 | \if{html}{\out{
}} 60 | \if{html}{\out{}} 61 | \if{latex}{\out{\hypertarget{method-Browser-get_host}{}}} 62 | \subsection{Method \code{get_host()}}{ 63 | Browser Host 64 | \subsection{Usage}{ 65 | \if{html}{\out{
}}\preformatted{Browser$get_host()}\if{html}{\out{
}} 66 | } 67 | 68 | } 69 | \if{html}{\out{
}} 70 | \if{html}{\out{}} 71 | \if{latex}{\out{\hypertarget{method-Browser-get_port}{}}} 72 | \subsection{Method \code{get_port()}}{ 73 | Browser port 74 | \subsection{Usage}{ 75 | \if{html}{\out{
}}\preformatted{Browser$get_port()}\if{html}{\out{
}} 76 | } 77 | 78 | } 79 | \if{html}{\out{
}} 80 | \if{html}{\out{}} 81 | \if{latex}{\out{\hypertarget{method-Browser-close}{}}} 82 | \subsection{Method \code{close()}}{ 83 | Close the browser 84 | \subsection{Usage}{ 85 | \if{html}{\out{
}}\preformatted{Browser$close(wait = FALSE)}\if{html}{\out{
}} 86 | } 87 | 88 | \subsection{Arguments}{ 89 | \if{html}{\out{
}} 90 | \describe{ 91 | \item{\code{wait}}{If an integer, waits a number of seconds for the process to 92 | exit, killing the process if it takes longer than \code{wait} seconds to 93 | close. Use \code{wait = TRUE} to wait for 10 seconds.} 94 | } 95 | \if{html}{\out{
}} 96 | } 97 | } 98 | \if{html}{\out{
}} 99 | \if{html}{\out{}} 100 | \if{latex}{\out{\hypertarget{method-Browser-clone}{}}} 101 | \subsection{Method \code{clone()}}{ 102 | The objects of this class are cloneable with this method. 103 | \subsection{Usage}{ 104 | \if{html}{\out{
}}\preformatted{Browser$clone(deep = FALSE)}\if{html}{\out{
}} 105 | } 106 | 107 | \subsection{Arguments}{ 108 | \if{html}{\out{
}} 109 | \describe{ 110 | \item{\code{deep}}{Whether to make a deep clone.} 111 | } 112 | \if{html}{\out{
}} 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /man/Chrome.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chrome.R 3 | \name{Chrome} 4 | \alias{Chrome} 5 | \title{Local Chrome process} 6 | \description{ 7 | This is a subclass of \code{\link{Browser}} that represents a local browser. It extends 8 | the \code{\link{Browser}} class with a \code{\link[processx:process]{processx::process}} object, which represents 9 | the browser's system process. 10 | } 11 | \seealso{ 12 | \code{\link[=get_chrome_args]{get_chrome_args()}} 13 | } 14 | \section{Super class}{ 15 | \code{\link[chromote:Browser]{chromote::Browser}} -> \code{Chrome} 16 | } 17 | \section{Methods}{ 18 | \subsection{Public methods}{ 19 | \itemize{ 20 | \item \href{#method-Chrome-new}{\code{Chrome$new()}} 21 | \item \href{#method-Chrome-get_path}{\code{Chrome$get_path()}} 22 | \item \href{#method-Chrome-clone}{\code{Chrome$clone()}} 23 | } 24 | } 25 | \if{html}{\out{ 26 |
Inherited methods 27 | 35 |
36 | }} 37 | \if{html}{\out{
}} 38 | \if{html}{\out{}} 39 | \if{latex}{\out{\hypertarget{method-Chrome-new}{}}} 40 | \subsection{Method \code{new()}}{ 41 | Create a new Chrome object. 42 | \subsection{Usage}{ 43 | \if{html}{\out{
}}\preformatted{Chrome$new(path = find_chrome(), args = get_chrome_args())}\if{html}{\out{
}} 44 | } 45 | 46 | \subsection{Arguments}{ 47 | \if{html}{\out{
}} 48 | \describe{ 49 | \item{\code{path}}{Location of chrome installation} 50 | 51 | \item{\code{args}}{A character vector of command-line arguments passed when 52 | initializing Chrome. Single on-off arguments are passed as single 53 | values (e.g.\code{"--disable-gpu"}), arguments with a value are given with a 54 | nested character vector (e.g. \code{c("--force-color-profile", "srgb")}). 55 | See 56 | \href{https://peter.sh/experiments/chromium-command-line-switches/}{here} 57 | for a list of possible arguments. Defaults to \code{\link[=get_chrome_args]{get_chrome_args()}}.} 58 | } 59 | \if{html}{\out{
}} 60 | } 61 | \subsection{Returns}{ 62 | A new \code{Chrome} object. 63 | } 64 | } 65 | \if{html}{\out{
}} 66 | \if{html}{\out{}} 67 | \if{latex}{\out{\hypertarget{method-Chrome-get_path}{}}} 68 | \subsection{Method \code{get_path()}}{ 69 | Browser application path 70 | \subsection{Usage}{ 71 | \if{html}{\out{
}}\preformatted{Chrome$get_path()}\if{html}{\out{
}} 72 | } 73 | 74 | } 75 | \if{html}{\out{
}} 76 | \if{html}{\out{}} 77 | \if{latex}{\out{\hypertarget{method-Chrome-clone}{}}} 78 | \subsection{Method \code{clone()}}{ 79 | The objects of this class are cloneable with this method. 80 | \subsection{Usage}{ 81 | \if{html}{\out{
}}\preformatted{Chrome$clone(deep = FALSE)}\if{html}{\out{
}} 82 | } 83 | 84 | \subsection{Arguments}{ 85 | \if{html}{\out{
}} 86 | \describe{ 87 | \item{\code{deep}}{Whether to make a deep clone.} 88 | } 89 | \if{html}{\out{
}} 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /man/ChromeRemote.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chrome.R 3 | \name{ChromeRemote} 4 | \alias{ChromeRemote} 5 | \title{Remote Chrome process} 6 | \description{ 7 | Remote Chrome process 8 | } 9 | \section{Super class}{ 10 | \code{\link[chromote:Browser]{chromote::Browser}} -> \code{ChromeRemote} 11 | } 12 | \section{Methods}{ 13 | \subsection{Public methods}{ 14 | \itemize{ 15 | \item \href{#method-ChromeRemote-new}{\code{ChromeRemote$new()}} 16 | \item \href{#method-ChromeRemote-is_alive}{\code{ChromeRemote$is_alive()}} 17 | \item \href{#method-ChromeRemote-close}{\code{ChromeRemote$close()}} 18 | \item \href{#method-ChromeRemote-clone}{\code{ChromeRemote$clone()}} 19 | } 20 | } 21 | \if{html}{\out{ 22 |
Inherited methods 23 | 29 |
30 | }} 31 | \if{html}{\out{
}} 32 | \if{html}{\out{}} 33 | \if{latex}{\out{\hypertarget{method-ChromeRemote-new}{}}} 34 | \subsection{Method \code{new()}}{ 35 | Create a new ChromeRemote object. 36 | \subsection{Usage}{ 37 | \if{html}{\out{
}}\preformatted{ChromeRemote$new(host, port)}\if{html}{\out{
}} 38 | } 39 | 40 | \subsection{Arguments}{ 41 | \if{html}{\out{
}} 42 | \describe{ 43 | \item{\code{host}}{A string that is a valid IPv4 or IPv6 address. \code{"0.0.0.0"} 44 | represents all IPv4 addresses and \code{"::/0"} represents all IPv6 addresses.} 45 | 46 | \item{\code{port}}{A number or integer that indicates the server port.} 47 | } 48 | \if{html}{\out{
}} 49 | } 50 | } 51 | \if{html}{\out{
}} 52 | \if{html}{\out{}} 53 | \if{latex}{\out{\hypertarget{method-ChromeRemote-is_alive}{}}} 54 | \subsection{Method \code{is_alive()}}{ 55 | Is the remote service alive? 56 | \subsection{Usage}{ 57 | \if{html}{\out{
}}\preformatted{ChromeRemote$is_alive()}\if{html}{\out{
}} 58 | } 59 | 60 | } 61 | \if{html}{\out{
}} 62 | \if{html}{\out{}} 63 | \if{latex}{\out{\hypertarget{method-ChromeRemote-close}{}}} 64 | \subsection{Method \code{close()}}{ 65 | chromote does not manage remote processes, so closing a 66 | remote Chrome browser does nothing. You can send a \code{Browser$close()} 67 | command if this is really something you want to do. 68 | \subsection{Usage}{ 69 | \if{html}{\out{
}}\preformatted{ChromeRemote$close()}\if{html}{\out{
}} 70 | } 71 | 72 | } 73 | \if{html}{\out{
}} 74 | \if{html}{\out{}} 75 | \if{latex}{\out{\hypertarget{method-ChromeRemote-clone}{}}} 76 | \subsection{Method \code{clone()}}{ 77 | The objects of this class are cloneable with this method. 78 | \subsection{Usage}{ 79 | \if{html}{\out{
}}\preformatted{ChromeRemote$clone(deep = FALSE)}\if{html}{\out{
}} 80 | } 81 | 82 | \subsection{Arguments}{ 83 | \if{html}{\out{
}} 84 | \describe{ 85 | \item{\code{deep}}{Whether to make a deep clone.} 86 | } 87 | \if{html}{\out{
}} 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /man/chrome_versions.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/manage.R 3 | \name{chrome_versions} 4 | \alias{chrome_versions} 5 | \alias{chrome_versions_path_cache} 6 | \alias{chrome_versions_path} 7 | \alias{chrome_versions_add} 8 | \alias{chrome_versions_remove} 9 | \title{Chrome versions cache helpers} 10 | \usage{ 11 | chrome_versions_path_cache(...) 12 | 13 | chrome_versions_path(version = "latest", binary = "chrome", platform = NULL) 14 | 15 | chrome_versions_add(version, binary, platform = NULL) 16 | 17 | chrome_versions_remove(version, binary, platform = NULL, ask = TRUE) 18 | } 19 | \arguments{ 20 | \item{...}{Additional path parts.} 21 | 22 | \item{version}{A character string specifying the version to list, add or 23 | remove.} 24 | 25 | \item{binary}{A character string specifying which binary to list. Defaults to 26 | \code{"all"} to show all binaries, or can be one or more of of \code{"chrome"}, 27 | \code{"chrome-headless-shell"}, or \code{"chromedriver"}.} 28 | 29 | \item{platform}{A character string specifying the platform(s) to list. If 30 | \code{NULL} (default), the platform will be automatically detected, or if 31 | \code{"all"}, then binaries for all platforms will be listed.} 32 | 33 | \item{ask}{Whether to ask before removing files.} 34 | } 35 | \value{ 36 | A character vector of Chrome binary paths. 37 | } 38 | \description{ 39 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#Experimental}{\figure{lifecycle-experimental.svg}{options: alt='[E]'}}}{\strong{[E]}} 40 | 41 | These functions help interact with the cache used by \pkg{chromote}'s for 42 | storing versioned Chrome for Testing binaries: 43 | \itemize{ 44 | \item \code{chrome_versions_path()}: Returns a path or paths to specific Chrome 45 | binaries in the cache. 46 | \item \code{chrome_versions_add()}: Add a specific version to the Chrome versions 47 | cache. 48 | \item \code{chrome_versions_remove()}: Remove specific versions and binaries from the 49 | Chrome cache. The \code{version}, \code{binary} and \code{platform} arguments can each 50 | take \code{"all"} to remove all installed copies of that version, binary or 51 | platform. 52 | \item \code{chrome_versions_path_cache()}: Returns the path to the cache directory 53 | used for Chrome binaries. 54 | } 55 | 56 | Managed Chrome installations is an experimental feature introduced in 57 | chromote v0.5.0 and was inspired by similar features in 58 | \href{https://playwright.dev/}{playwright}. 59 | } 60 | \seealso{ 61 | \code{\link[=chrome_versions_list]{chrome_versions_list()}} 62 | } 63 | -------------------------------------------------------------------------------- /man/chrome_versions_list.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/manage.R 3 | \name{chrome_versions_list} 4 | \alias{chrome_versions_list} 5 | \title{List installed or available Chrome binary versions} 6 | \usage{ 7 | chrome_versions_list( 8 | which = c("installed", "all"), 9 | binary = c("all", "chrome", "chrome-headless-shell", "chromedriver"), 10 | platform = NULL 11 | ) 12 | } 13 | \arguments{ 14 | \item{which}{Whether to list \code{"installed"} local binaries or to list \code{"all"} 15 | chrome versions available from online sources.} 16 | 17 | \item{binary}{A character string specifying which binary to list. Defaults to 18 | \code{"all"} to show all binaries, or can be one or more of of \code{"chrome"}, 19 | \code{"chrome-headless-shell"}, or \code{"chromedriver"}.} 20 | 21 | \item{platform}{A character string specifying the platform(s) to list. If 22 | \code{NULL} (default), the platform will be automatically detected, or if 23 | \code{"all"}, then binaries for all platforms will be listed.} 24 | } 25 | \value{ 26 | Returns a \code{\link[=data.frame]{data.frame()}} of Chrome for Testing versions with 27 | columns: \code{version}, \code{revision}, \code{binary}, \code{platform}, \code{url} (where the 28 | binary can be downloaded), and--if \code{which = "installed"}--the local path to 29 | the binary in the \code{\link[=chrome_versions_path_cache]{chrome_versions_path_cache()}}. 30 | } 31 | \description{ 32 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#Experimental}{\figure{lifecycle-experimental.svg}{options: alt='[E]'}}}{\strong{[E]}} 33 | 34 | By default lists the installed Chrome versions in the \code{\link[=chrome_versions_path_cache]{chrome_versions_path_cache()}}, 35 | or list all Chrome versions available via Google's 36 | \href{https://googlechromelabs.github.io/chrome-for-testing/}{Chrome for Testing} 37 | service. 38 | 39 | Managed Chrome installations is an experimental feature introduced in 40 | chromote v0.5.0 and was inspired by similar features in 41 | \href{https://playwright.dev/}{playwright}. 42 | } 43 | \examples{ 44 | \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 45 | chrome_versions_list() 46 | \dontshow{\}) # examplesIf} 47 | } 48 | -------------------------------------------------------------------------------- /man/chromote-options.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chromote-package.R 3 | \name{chromote-options} 4 | \alias{chromote-options} 5 | \title{chromote Options} 6 | \description{ 7 | These options and environment variables that are used by chromote. Options 8 | are lowercase and can be set with \code{options()}. Environment variables are 9 | uppercase and can be set in an \code{.Renviron} file, with \code{Sys.setenv()}, or in 10 | the shell or process running R. If both an option or environment variable are 11 | supported, chromote will use the option first. 12 | \itemize{ 13 | \item \code{CHROMOTE_CHROME} \cr 14 | Path to the Chrome executable. If not set, chromote will 15 | attempt to find and use the system installation of Chrome. 16 | \item \code{chromote.headless}, \code{CHROMOTE_HEADLESS} \cr 17 | Headless mode for Chrome. Can be \code{"old"} or \code{"new"}. See 18 | \href{https://developer.chrome.com/docs/chromium/new-headless}{Chrome Headless mode} 19 | for more details. 20 | \item \code{chromote.timeout} \cr 21 | Timeout (in seconds) for Chrome to launch or connect. Default is \code{10}. 22 | \item \code{chromote.launch.echo_cmd} \cr 23 | Echo the command used to launch Chrome to the console for debugging. 24 | Default is \code{FALSE}. 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /man/chromote-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chromote-package.R 3 | \docType{package} 4 | \name{chromote-package} 5 | \alias{chromote} 6 | \alias{chromote-package} 7 | \title{chromote: Headless Chrome Web Browser Interface} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | An implementation of the 'Chrome DevTools Protocol', for controlling a headless Chrome web browser. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://rstudio.github.io/chromote/} 17 | \item \url{https://github.com/rstudio/chromote} 18 | \item Report bugs at \url{https://github.com/rstudio/chromote/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: Garrick Aden-Buie \email{garrick@posit.co} (\href{https://orcid.org/0000-0002-7111-0077}{ORCID}) 24 | 25 | Authors: 26 | \itemize{ 27 | \item Winston Chang \email{winston@posit.co} 28 | \item Barret Schloerke \email{barret@posit.co} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID}) 29 | } 30 | 31 | Other contributors: 32 | \itemize{ 33 | \item Posit Software, PBC (03wc8by49) [copyright holder, funder] 34 | } 35 | 36 | } 37 | \keyword{internal} 38 | -------------------------------------------------------------------------------- /man/chromote_info.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chrome.R 3 | \name{chromote_info} 4 | \alias{chromote_info} 5 | \title{Show information about the chromote package and Chrome browser} 6 | \usage{ 7 | chromote_info() 8 | } 9 | \value{ 10 | A list containing the following elements: 11 | \describe{ 12 | \item{os}{The operating system platform.} 13 | \item{version_r}{The version of R.} 14 | \item{version_chromote}{The version of the chromote package.} 15 | \item{envvar}{The value of the \code{CHROMOTE_CHROME} environment variable.} 16 | \item{path}{The path to the Chrome browser.} 17 | \item{args}{A vector of Chrome arguments.} 18 | \item{version}{The version of Chrome (if verification is successful).} 19 | \item{error}{The error message (if verification fails).} 20 | \item{.check}{A list with the status and output of the Chrome verification.} 21 | } 22 | } 23 | \description{ 24 | This function gathers information about the operating system, R version, 25 | chromote package version, environment variables, Chrome path, and Chrome 26 | arguments. It also verifies the Chrome installation and retrieves its version. 27 | } 28 | \examples{ 29 | chromote_info() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /man/default_chrome_args.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chromote.R 3 | \name{default_chrome_args} 4 | \alias{default_chrome_args} 5 | \alias{get_chrome_args} 6 | \alias{set_chrome_args} 7 | \title{Default Chrome arguments} 8 | \usage{ 9 | default_chrome_args() 10 | 11 | get_chrome_args() 12 | 13 | set_chrome_args(args) 14 | } 15 | \arguments{ 16 | \item{args}{A character vector of command-line arguments (or \code{NULL}) to be 17 | used with every new \code{\link{ChromoteSession}}.} 18 | } 19 | \value{ 20 | A character vector of default command-line arguments to be used with 21 | every new \code{\link{ChromoteSession}} 22 | } 23 | \description{ 24 | A character vector of command-line arguments passed when initializing any new 25 | instance of \code{\link{Chrome}}. Single on-off arguments are passed as single values 26 | (e.g.\code{"--disable-gpu"}), arguments with a value are given with a nested 27 | character vector (e.g. \code{c("--force-color-profile", "srgb")}). See 28 | \href{https://peter.sh/experiments/chromium-command-line-switches/}{here} for a 29 | list of possible arguments. 30 | } 31 | \details{ 32 | Default chromote arguments are composed of the following values (when 33 | appropriate): 34 | \itemize{ 35 | \item \href{https://peter.sh/experiments/chromium-command-line-switches/#disable-gpu}{\code{"--disable-gpu"}} 36 | \itemize{ 37 | \item Only added on Windows, as empirically it appears to be needed 38 | (if not, check runs on GHA never terminate). 39 | \item Disables GPU hardware acceleration. If software renderer is not in place, then the GPU process won't launch. 40 | } 41 | \item \href{https://peter.sh/experiments/chromium-command-line-switches/#no-sandbox}{\code{"--no-sandbox"}} 42 | \itemize{ 43 | \item Only added when \code{CI} system environment variable is set, when the 44 | user on a Linux system is not set, or when executing inside a Docker container. 45 | \item Disables the sandbox for all process types that are normally sandboxed. Meant to be used as a browser-level switch for testing purposes only 46 | } 47 | \item \href{https://peter.sh/experiments/chromium-command-line-switches/#disable-dev-shm-usage}{\code{"--disable-dev-shm-usage"}} 48 | \itemize{ 49 | \item Only added when \code{CI} system environment variable is set or when inside a docker instance. 50 | \item The \verb{/dev/shm} partition is too small in certain VM environments, causing Chrome to fail or crash. 51 | } 52 | \item \href{https://peter.sh/experiments/chromium-command-line-switches/#force-color-profile}{\code{"--force-color-profile=srgb"}} 53 | \itemize{ 54 | \item This means that screenshots taken on a laptop plugged into an external 55 | monitor will often have subtly different colors than one taken when 56 | the laptop is using its built-in monitor. This problem will be even 57 | more likely across machines. 58 | \item Force all monitors to be treated as though they have the specified color profile. 59 | } 60 | \item \href{https://peter.sh/experiments/chromium-command-line-switches/#disable-extensions}{\code{"--disable-extensions"}} 61 | \itemize{ 62 | \item Disable extensions. 63 | } 64 | \item \href{https://peter.sh/experiments/chromium-command-line-switches/#mute-audio}{\code{"--mute-audio"}} 65 | \itemize{ 66 | \item Mutes audio sent to the audio device so it is not audible during automated testing. 67 | } 68 | } 69 | } 70 | \section{Functions}{ 71 | \itemize{ 72 | \item \code{default_chrome_args()}: Returns a character vector of command-line 73 | arguments passed when initializing Chrome. See Details for more 74 | information. 75 | 76 | \item \code{get_chrome_args()}: Retrieves the default command-line arguments 77 | passed to \code{\link{Chrome}} during initialization. Returns either \code{NULL} or a 78 | character vector. 79 | 80 | \item \code{set_chrome_args()}: Sets the default command-line arguments 81 | passed when initializing. Returns the updated defaults. 82 | 83 | }} 84 | \examples{ 85 | old_chrome_args <- get_chrome_args() 86 | 87 | # Disable the gpu and use of `/dev/shm` 88 | set_chrome_args(c("--disable-gpu", "--disable-dev-shm-usage")) 89 | 90 | #... Make new `Chrome` or `ChromoteSession` instance 91 | 92 | # Restore old defaults 93 | set_chrome_args(old_chrome_args) 94 | } 95 | -------------------------------------------------------------------------------- /man/default_chromote_object.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chromote.R 3 | \name{default_chromote_object} 4 | \alias{default_chromote_object} 5 | \alias{has_default_chromote_object} 6 | \alias{set_default_chromote_object} 7 | \title{Default Chromote object} 8 | \usage{ 9 | default_chromote_object() 10 | 11 | has_default_chromote_object() 12 | 13 | set_default_chromote_object(x) 14 | } 15 | \arguments{ 16 | \item{x}{A \link{Chromote} object.} 17 | } 18 | \description{ 19 | Returns the Chromote package's default \link{Chromote} object. If 20 | there is not currently a default \code{Chromote} object that is active, then 21 | one will be created and set as the default. 22 | } 23 | \details{ 24 | \code{ChromoteSession$new()} calls this function by default, if the 25 | \code{parent} is not specified. That means that when 26 | \code{ChromoteSession$new()} is called and there is not currently an 27 | active default \code{Chromote} object, then a new \code{Chromote} object will 28 | be created and set as the default. 29 | } 30 | -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: experimental 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | experimental 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/man/figures/logo.png -------------------------------------------------------------------------------- /man/figures/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/man/figures/sidebar.png -------------------------------------------------------------------------------- /man/find_chrome.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/chrome.R 3 | \name{find_chrome} 4 | \alias{find_chrome} 5 | \title{Find path to Chrome or Chromium browser} 6 | \usage{ 7 | find_chrome() 8 | } 9 | \value{ 10 | A character vector with the value of \code{CHROMOTE_CHROME}, or a path to 11 | the discovered Chrome executable. If no path to is found, \code{find_chrome()} 12 | returns \code{NULL}. 13 | } 14 | \description{ 15 | \pkg{chromote} requires a Chrome- or Chromium-based browser with support for 16 | the Chrome DevTools Protocol. There are many such browser variants, 17 | including \href{https://www.google.com/chrome/}{Google Chrome}, 18 | \href{https://www.chromium.org/chromium-projects/}{Chromium}, 19 | \href{https://www.microsoft.com/en-us/edge}{Microsoft Edge} and others. 20 | 21 | If you want \pkg{chromote} to use a specific browser, set the 22 | \code{CHROMOTE_CHROME} environment variable to the full path to the browser's 23 | executable. Note that when \code{CHROMOTE_CHROME} is set, \pkg{chromote} will use 24 | the value without any additional checks. On Mac, for example, one could use 25 | Microsoft Edge by setting \code{CHROMOTE_CHROME} with the following: 26 | 27 | \if{html}{\out{
}}\preformatted{Sys.setenv( 28 | CHROMOTE_CHROME = "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" 29 | ) 30 | }\if{html}{\out{
}} 31 | 32 | When \code{CHROMOTE_CHROME} is not set, \code{find_chrome()} will perform a limited 33 | search to find a reasonable executable. On Windows, \code{find_chrome()} consults 34 | the registry to find \code{chrome.exe}. On Mac, it looks for \verb{Google Chrome} in 35 | the \verb{/Applications} folder (or tries the same checks as on Linux). On Linux, 36 | it searches for several common executable names. 37 | } 38 | \examples{ 39 | find_chrome() 40 | 41 | } 42 | -------------------------------------------------------------------------------- /man/fragments/basic-usage.Rmd: -------------------------------------------------------------------------------- 1 | ```{r} 2 | #| echo: false 3 | if (!exists("MAN_PATH")) MAN_PATH <- "man" 4 | ``` 5 | 6 | ## Basic usage 7 | 8 | This will start a headless browser and open an interactive viewer for it in a normal browser, so that you can see what the headless browser is doing. 9 | 10 | ```R 11 | library(chromote) 12 | 13 | b <- ChromoteSession$new() 14 | 15 | # In a web browser, open a viewer for the headless browser. Works best with 16 | # Chromium-based browsers. 17 | b$view() 18 | ``` 19 | 20 | The browser can be given _commands_, as specified by the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). For example, `$Browser$getVersion()` (which corresponds to the [Browser.getVersion](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-getVersion) in the API docs) will query the browser for version information: 21 | 22 | 23 | ```R 24 | b$Browser$getVersion() 25 | #> $protocolVersion 26 | #> [1] "1.3" 27 | #> 28 | #> $product 29 | #> [1] "HeadlessChrome/98.0.4758.102" 30 | #> 31 | #> $revision 32 | #> [1] "@273bf7ac8c909cde36982d27f66f3c70846a3718" 33 | #> 34 | #> $userAgent 35 | #> [1] "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/98.0.4758.102 Safari/537.36" 36 | #> 37 | #> $jsVersion 38 | #> [1] "9.8.177.11" 39 | ``` 40 | 41 | 42 | If you have the viewer open and run the following, you'll see the web page load in the viewer[^interactive]: 43 | 44 | ```R 45 | b$go_to("https://www.r-project.org/") 46 | ``` 47 | 48 | In addition to full support of the Chrome Devtools Protocol, `ChromoteSession` objects also have some convenience methods, like `$go_to()` and `$screenshot()`. (See the Examples section below for more information about screenshots.) 49 | 50 | ```R 51 | # Saves to screenshot.png 52 | b$screenshot() 53 | 54 | # Takes a screenshot of elements picked out by CSS selector 55 | b$screenshot("sidebar.png", selector = ".sidebar") 56 | ``` 57 | 58 | ![A screenshot of the sidebar of r-rproject.org, circa 2023.](`r MAN_PATH`/figures/sidebar.png) 59 | 60 | [^interactive]: This simple example works interactively, but if you're using chromote to programmatically take screenshots you'll want to read `vignette("example-loading-page")` for a consistent and reliable approach. 61 | -------------------------------------------------------------------------------- /man/fragments/features.Rmd: -------------------------------------------------------------------------------- 1 | Chromote is an R implementation of the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). It works with Chrome, Chromium, Opera, Vivaldi, and other browsers based on [Chromium](https://www.chromium.org/). By default it uses Google Chrome (which must already be installed on the system). To use a different browser, see `vignette("which-chrome")`. 2 | 3 | Chromote is not the only R package that implements the Chrome DevTools Protocol. Here are some others: 4 | 5 | * [crrri](https://github.com/RLesur/crrri) by Romain Lesur and Christophe Dervieux 6 | * [decapitated](https://github.com/hrbrmstr/decapitated/) by Bob Rudis 7 | * [chradle](https://github.com/milesmcbain/chradle) by Miles McBain 8 | 9 | The interface to Chromote is similar to [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) for node.js. 10 | 11 | ## Features 12 | 13 | * Install and use specific versions of Chrome from the [Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/) service. 14 | 15 | * Offers a synchronous API for ease of use and an asynchronous API for more sophisticated tasks. 16 | 17 | * Full support for the Chrome DevTools Protocol for any version of Chrome or any Chrome-based browser. 18 | 19 | * Includes convenience methods, like `$screenshot()` and `$set_viewport_size()`, for common tasks. 20 | 21 | * Automatically reconnects to previous sessions if the connection from R to Chrome is lost, for example when restarting from sleep state. 22 | 23 | * Powers many higher-level packages and functions, like `{shinytest2}` and `rvest::read_html_live()`. 24 | -------------------------------------------------------------------------------- /man/fragments/install.Rmd: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Install the released version of chromote from CRAN: 4 | 5 | ```{r, eval = FALSE} 6 | install.packages("chromote") 7 | ``` 8 | 9 | Or install the development version from GitHub with: 10 | 11 | ```{r, eval = FALSE} 12 | # install.packages("pak") 13 | pak::pak("rstudio/chromote") 14 | ``` 15 | -------------------------------------------------------------------------------- /man/reexports.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/promises.R 3 | \docType{import} 4 | \name{reexports} 5 | \alias{reexports} 6 | \alias{\%...>\%} 7 | \alias{\%...!\%} 8 | \alias{\%...T>\%} 9 | \alias{\%...T!\%} 10 | \alias{\%>\%} 11 | \alias{\%T>\%} 12 | \alias{promise} 13 | \alias{then} 14 | \alias{catch} 15 | \alias{finally} 16 | \title{Objects exported from other packages} 17 | \keyword{internal} 18 | \description{ 19 | These objects are imported from other packages. Follow the links 20 | below to see their documentation. 21 | 22 | \describe{ 23 | \item{magrittr}{\code{\link[magrittr:pipe]{\%>\%}}, \code{\link[magrittr:tee]{\%T>\%}}} 24 | 25 | \item{promises}{\code{\link[promises:pipes]{\%...!\%}}, \code{\link[promises:pipes]{\%...>\%}}, \code{\link[promises:pipes]{\%...T!\%}}, \code{\link[promises:pipes]{\%...T>\%}}, \code{\link[promises:then]{catch}}, \code{\link[promises:then]{finally}}, \code{\link[promises]{promise}}, \code{\link[promises]{then}}} 26 | }} 27 | 28 | -------------------------------------------------------------------------------- /man/with_chrome_version.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/manage.R 3 | \name{with_chrome_version} 4 | \alias{with_chrome_version} 5 | \alias{local_chrome_version} 6 | \alias{local_chromote_chrome} 7 | \alias{with_chromote_chrome} 8 | \title{Use a specific version of Chrome or related binaries} 9 | \usage{ 10 | with_chrome_version( 11 | version = "latest-stable", 12 | code, 13 | ..., 14 | binary = c("chrome", "chrome-headless-shell", "chromedriver"), 15 | platform = NULL, 16 | quiet = TRUE 17 | ) 18 | 19 | local_chrome_version( 20 | version = "latest-stable", 21 | binary = c("chrome", "chrome-headless-shell", "chromedriver"), 22 | platform = NULL, 23 | ..., 24 | quiet = FALSE, 25 | .local_envir = parent.frame() 26 | ) 27 | 28 | local_chromote_chrome(path, ..., .local_envir = parent.frame()) 29 | 30 | with_chromote_chrome(path, code, ...) 31 | } 32 | \arguments{ 33 | \item{version}{A character string specifying the version to use. The default 34 | value is \code{"latest-stable"} to follow the latest stable release of Chrome. 35 | For robust results, and to avoid frequently downloading new versions of 36 | Chrome, use a fully qualified version number, e.g. \code{"133.0.6943.141"}. 37 | 38 | If you specify a partial version, e.g. \code{"133"}, chromote will find the most 39 | recent release matching that version, preferring to use the latest 40 | \emph{installed} release that matches the partially-specified version. chromote 41 | also supports a few special version names: 42 | \itemize{ 43 | \item \code{"latest-installed"}: The latest version currently installed locally in 44 | chromote's cache. If you don't have any installed versions of the binary, 45 | chromote uses \code{"latest"}. 46 | \item \code{"latest"}: The most recent Chrome for Testing release, which may be a 47 | beta or canary release. 48 | \item \code{"latest-stable"}, \code{"latest-beta"}, \code{"latest-extended"}, 49 | \code{"latest-canary"} or \code{"latest-dev"}: Installs the latest release from one 50 | of Chrome's version channels, queried from the 51 | \href{https://developer.chrome.com/docs/web-platform/versionhistory/reference#platform-identifiers}{VersionHistory API}. 52 | \code{"latest-stable"} is the default value of \code{with_chrome_version()} and 53 | \code{local_chrome_version()}. 54 | \item \code{"system"}: Use the system-wide installation of Chrome. 55 | } 56 | 57 | Chromote also supports} 58 | 59 | \item{code}{\code{[any]}\cr Code to execute in the temporary environment} 60 | 61 | \item{...}{Ignored, used to require named arguments and for future feature 62 | expansion.} 63 | 64 | \item{binary}{A character string specifying which binary to 65 | use. Must be one of \code{"chrome"}, \code{"chrome-headless-shell"}, or 66 | \code{"chromedriver"}. Default is \code{"chrome"}.} 67 | 68 | \item{platform}{A character string specifying the platform. If \code{NULL} 69 | (default), the platform will be automatically detected.} 70 | 71 | \item{quiet}{Whether to print a message indicating which version and binary 72 | of Chrome is being used. By default, this message is suppressed for 73 | \code{\link[=with_chrome_version]{with_chrome_version()}} and enabled for \code{\link[=local_chrome_version]{local_chrome_version()}}.} 74 | 75 | \item{.local_envir}{\verb{[environment]}\cr The environment to use for scoping.} 76 | 77 | \item{path}{A direct path to the Chrome (or Chrome-based) binary. See 78 | \code{\link[=find_chrome]{find_chrome()}} for details or \code{\link[=chrome_versions_path]{chrome_versions_path()}} for paths 79 | from the chromote-managed cache.} 80 | } 81 | \value{ 82 | Temporarily sets the \code{CHROMOTE_CHROME} environment variable and 83 | returns the result of the \code{code} argument. 84 | } 85 | \description{ 86 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#Experimental}{\figure{lifecycle-experimental.svg}{options: alt='[E]'}}}{\strong{[E]}} 87 | 88 | This function downloads and sets up a specific version of Chrome, using the 89 | \href{https://googlechromelabs.github.io/chrome-for-testing/}{Google Chrome for Testing builds} 90 | for \code{chrome}, \code{chrome-headless-shell} or \code{chromedriver} for use with 91 | chromote. 92 | 93 | Managed Chrome installations is an experimental feature introduced in 94 | chromote v0.5.0 and was inspired by similar features in 95 | \href{https://playwright.dev/}{playwright}. 96 | } 97 | \details{ 98 | This function downloads the specified binary, if not already 99 | available and configures \code{\link[=find_chrome]{find_chrome()}} to use the specified binary while 100 | evaluating \code{code} or within the local scope. It uses the 101 | "known-good-versions" list from the Google Chrome for Testing versions at 102 | \url{https://googlechromelabs.github.io/chrome-for-testing/}. 103 | } 104 | \section{Functions}{ 105 | \itemize{ 106 | \item \code{with_chrome_version()}: Temporarily use a specific version of Chrome 107 | during the evaluation of \code{code}. 108 | 109 | \item \code{local_chrome_version()}: Use a specific version of Chrome within the 110 | current scope. 111 | 112 | \item \code{local_chromote_chrome()}: Use a specific Chrome, by path, within the 113 | current scope. 114 | 115 | \item \code{with_chromote_chrome()}: Temporarily use a specific Chrome version, by 116 | path, for the evaluation of \code{code}. 117 | 118 | }} 119 | \examples{ 120 | \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 121 | # Use the latest version of Chrome 122 | local_chrome_version() 123 | 124 | # Use a specific version of chrome-headless-shell 125 | local_chrome_version("114.0.5735.90", binary = "chrome-headless-shell") 126 | \dontshow{\}) # examplesIf} 127 | } 128 | -------------------------------------------------------------------------------- /pkgdown/_brand.yml: -------------------------------------------------------------------------------- 1 | color: 2 | palette: 3 | blue: "#007bc2" 4 | indigo: "#4b00c1" 5 | purple: "#74149c" 6 | pink: "#bf007f" 7 | red: "#c10000" 8 | orange: "#f45100" 9 | yellow: "#f9b928" 10 | green: "#00891a" 11 | teal: "#00bf7f" 12 | cyan: "#03c7e8" 13 | white: "#ffffff" 14 | black: "#1D1F21" 15 | 16 | foreground: black 17 | background: white 18 | primary: blue 19 | secondary: gray 20 | success: green 21 | info: cyan 22 | warning: yellow 23 | danger: red 24 | light: "#f8f8f8" 25 | dark: "#212529" 26 | 27 | typography: 28 | fonts: 29 | - family: Open Sans 30 | source: bunny 31 | - family: Source Code Pro 32 | source: bunny 33 | 34 | headings: 35 | family: Open Sans 36 | weight: 300 37 | monospace: Source Code Pro 38 | monospace-inline: 39 | color: pink 40 | background-color: transparent 41 | size: 1em 42 | 43 | defaults: 44 | bootstrap: 45 | defaults: 46 | navbar-bg: $brand-blue 47 | code-color-dark: "#fa88d4" 48 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://rstudio.github.io/chromote 2 | 3 | authors: 4 | Posit Software, PBC: 5 | href: https://www.posit.co 6 | html: >- 7 | Posit 8 | Barret Schloerke: 9 | href: http://schloerke.com 10 | Garrick Aden-Buie: 11 | href: https://garrickadenbuie.com 12 | Winston Chang: 13 | href: https://github.com/wch 14 | 15 | template: 16 | bootstrap: 5 17 | light-switch: true 18 | theme: github-light 19 | theme-dark: github-dark 20 | bslib: 21 | brand: pkgdown/_brand.yml 22 | 23 | development: 24 | mode: auto 25 | 26 | articles: 27 | - title: Learn chromote 28 | navbar: ~ 29 | contents: 30 | - chromote 31 | - commands-and-events 32 | - sync-async 33 | - which-chrome 34 | - title: Examples 35 | navbar: Examples 36 | contents: 37 | - example-loading-page 38 | - example-screenshot 39 | - example-extract-text 40 | - starts_with("example-") 41 | 42 | reference: 43 | - title: Chromote Sessions 44 | # desc: ~ 45 | contents: 46 | - ChromoteSession 47 | - Chromote 48 | 49 | - title: Default settings 50 | # desc: ~ 51 | contents: 52 | - chromote-options 53 | - chromote_info 54 | - default_chrome_args 55 | - default_chromote_object 56 | 57 | - title: Browsers 58 | # desc: ~ 59 | contents: 60 | - find_chrome 61 | - Browser 62 | - Chrome 63 | - ChromeRemote 64 | 65 | - title: Manage and install Chrome for Testing 66 | desc: | 67 | Download and use any version of Chrome or `chrome-headless-shell` available 68 | via the [Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/) 69 | service. 70 | contents: 71 | - with_chrome_version 72 | - chrome_versions_list 73 | - chrome_versions 74 | 75 | news: 76 | releases: 77 | - text: "v0.5.0" 78 | href: https://shiny.posit.co/blog/posts/chromote-0.5.0/ 79 | -------------------------------------------------------------------------------- /pkgdown/extra.scss: -------------------------------------------------------------------------------- 1 | html[data-bs-theme="dark"] code { 2 | background-color: transparent; 3 | } 4 | 5 | .navbar-brand+.nav-text { 6 | color: var(--bs-navbar-color) !important; 7 | } 8 | 9 | code a:any-link { 10 | text-decoration-color: currentColor !important; 11 | } -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/pkgdown/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /pkgdown/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /pkgdown/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/pkgdown/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /pkgdown/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/chromote/13a789a2c2f9bae17687cb11b724e6c26ff46ada/pkgdown/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /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 | # Revdeps 2 | 3 | ## Failed to check (1) 4 | 5 | |package |version |error |warning |note | 6 | |:----------|:-------|:-----|:-------|:----| 7 | |renderthis |? | | | | 8 | 9 | -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 25 reverse dependencies (24 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 | # renderthis 2 | 3 |
4 | 5 | * Version: NA 6 | * GitHub: NA 7 | * Source code: https://github.com/cran/renderthis 8 | * Number of recursive dependencies: 77 9 | 10 | Run `revdepcheck::cloud_details(, "renderthis")` for more info 11 | 12 |
13 | 14 | ## Error before installation 15 | 16 | ### Devel 17 | 18 | ``` 19 | 20 | 21 | 22 | 23 | 24 | 25 | ``` 26 | ### CRAN 27 | 28 | ``` 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(chromote) 3 | 4 | test_check("chromote") 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/chromote_session.md: -------------------------------------------------------------------------------- 1 | # ChromoteSession auto_events_enable_args errors 2 | 3 | Code 4 | chromote_session$auto_events_enable_args("Browser", no_enable = TRUE) 5 | Condition 6 | Error in `chromote_session$auto_events_enable_args()`: 7 | ! Browser does not have an enable method. 8 | 9 | --- 10 | 11 | Code 12 | chromote_session$auto_events_enable_args("Animation", bad = TRUE) 13 | Condition 14 | Error in `chromote_session$auto_events_enable_args()`: 15 | ! Animation.enable does not have argument: `bad`. 16 | i Available arguments: `callback_`, `error_`, and `timeout_` 17 | 18 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/linux64/manage.md: -------------------------------------------------------------------------------- 1 | # with_chrome_version() works 2 | 3 | Code 4 | with_chrome_version("128.0.6612.0", with_retries(try_chromote_info)) 5 | Output 6 | $path 7 | [1] "~/.cache/R/chromote/chrome/128.0.6612.0/chrome-linux64/chrome" 8 | 9 | $version 10 | [1] "Google Chrome for Testing 128.0.6612.0" 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/mac-arm64/manage.md: -------------------------------------------------------------------------------- 1 | # with_chrome_version() works 2 | 3 | Code 4 | with_chrome_version("128.0.6612.0", with_retries(try_chromote_info)) 5 | Output 6 | $path 7 | [1] "~/Library/Caches/org.R-project.R/R/chromote/chrome/128.0.6612.0/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" 8 | 9 | $version 10 | [1] "Google Chrome for Testing 128.0.6612.0" 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/win64/manage.md: -------------------------------------------------------------------------------- 1 | # with_chrome_version() works 2 | 3 | Code 4 | with_chrome_version("128.0.6612.0", with_retries(try_chromote_info)) 5 | Output 6 | $path 7 | [1] "C:/Users/runneradmin/AppData/Local/R/cache/R/chromote/chrome/128.0.6612.0/chrome-win64/chrome.exe" 8 | 9 | $version 10 | [1] "128.0.6612.0" 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | skip_if_no_chromote <- function() { 2 | skip_on_cran() 3 | skip_if(lacks_chromote(), "chromote not available") 4 | } 5 | 6 | lacks_chromote <- function() { 7 | # We try twice because in particular Windows on GHA seems to need it, 8 | # but it doesn't otherwise hurt. More details at 9 | # https://github.com/rstudio/shinytest2/issues/209 10 | env_cache(globals, "lacks_chromote", !has_chromote() && !has_chromote()) 11 | } 12 | 13 | has_chromote <- function() { 14 | tryCatch( 15 | { 16 | default <- default_chromote_object() 17 | local_bindings(default_timeout = 5, .env = default) 18 | startup <- default$new_session(wait_ = FALSE) 19 | default$wait_for(startup) 20 | TRUE 21 | }, 22 | error = function(cnd) { 23 | FALSE 24 | } 25 | ) 26 | } 27 | 28 | with_retries <- function(fn, max_tries = 3) { 29 | trace <- trace_back() 30 | 31 | retry <- function(tried = 0) { 32 | tryCatch( 33 | { 34 | fn() 35 | }, 36 | error = function(err) { 37 | tried <- tried + 1 38 | if (tried >= max_tries) { 39 | rlang::abort( 40 | sprintf("Failed after %s tries", tried), 41 | parent = err, 42 | trace = trace 43 | ) 44 | } else { 45 | retry(tried) 46 | } 47 | } 48 | ) 49 | } 50 | 51 | retry() 52 | } 53 | -------------------------------------------------------------------------------- /tests/testthat/setup.R: -------------------------------------------------------------------------------- 1 | on_cran <- !isTRUE(as.logical(Sys.getenv("NOT_CRAN", "false"))) 2 | 3 | if (!on_cran) { 4 | has_chromote_envvar <- !identical(Sys.getenv("CHROMOTE_CHROME"), "") 5 | 6 | if (!has_chromote_envvar) { 7 | local_chrome_version("latest-stable", "chrome") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/testthat/test-chrome.R: -------------------------------------------------------------------------------- 1 | expect_true_eventually <- function(expr, max_tries = 50, delay = 0.1) { 2 | expr <- enquo(expr) 3 | 4 | expect_true( 5 | with_retries( 6 | function() { 7 | if (!eval_tidy(expr)) { 8 | Sys.sleep(delay) 9 | stop(expr_text(expr), " is not yet TRUE") 10 | } 11 | TRUE 12 | }, 13 | max_tries = max_tries 14 | ) 15 | ) 16 | } 17 | 18 | test_that("chrome with remote hosts", { 19 | skip_if_no_chromote() 20 | 21 | res <- with_random_port(function(port) { 22 | args <- c( 23 | get_chrome_args(), 24 | "--headless", 25 | "--remote-debugging-address=0.0.0.0", 26 | sprintf("--remote-debugging-port=%s", port) 27 | ) 28 | 29 | p <- processx::process$new(find_chrome(), args) 30 | list(port = port, process = p) 31 | }) 32 | 33 | withr::defer(if (!res$process$is_alive()) res$process$kill()) 34 | 35 | remote <- ChromeRemote$new(host = "localhost", port = res$port) 36 | 37 | expect_true_eventually(remote$is_alive()) 38 | expect_true(remote$close()) # does nothing but invisibly returns TRUE 39 | expect_true(remote$is_alive()) 40 | 41 | chromote <- Chromote$new(browser = remote) 42 | expect_true(chromote$is_alive()) 43 | expect_true(chromote$is_active()) 44 | 45 | tab <- ChromoteSession$new(parent = chromote) 46 | expect_true(tab$is_active()) 47 | 48 | # Close the websocket 49 | chromote$.__enclos_env__$private$ws$close() 50 | expect_true_eventually(!chromote$is_active()) 51 | expect_true_eventually(!tab$is_active()) 52 | 53 | # Reconnect 54 | tab2 <- suppressMessages(tab$respawn()) 55 | expect_true_eventually(chromote$is_active()) 56 | expect_true_eventually(tab2$is_active()) 57 | 58 | tab2$close() 59 | expect_false(tab$is_active()) 60 | tab2$parent$close() 61 | expect_true_eventually(!chromote$is_active()) 62 | expect_true(chromote$is_alive()) # still alive, we haven't killed the process yet 63 | 64 | res$process$kill() 65 | expect_true_eventually(!chromote$is_alive()) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/testthat/test-chromote_session.R: -------------------------------------------------------------------------------- 1 | test_that("respawning preserves targetId and auto_events", { 2 | skip_if_no_chromote() 3 | 4 | sess1 <- create_session(auto_events = FALSE) 5 | sess2 <- sess1$respawn() 6 | 7 | expect_equal(sess1$get_target_id(), sess2$get_target_id()) 8 | expect_equal(sess1$get_auto_events(), sess2$get_auto_events()) 9 | }) 10 | 11 | test_that("ChromoteSession track metrics from `Emulation.setDeviceMetricsOverride`", { 12 | skip_if_no_chromote() 13 | 14 | page <- ChromoteSession$new(mobile = TRUE) 15 | withr::defer(page$close()) 16 | 17 | expect_true(page$.__enclos_env__$private$is_mobile) 18 | 19 | page$Emulation$setDeviceMetricsOverride( 20 | 600, 21 | 600, 22 | deviceScaleFactor = 2, 23 | mobile = FALSE 24 | ) 25 | expect_false(page$.__enclos_env__$private$is_mobile) 26 | expect_equal(page$.__enclos_env__$private$pixel_ratio, 2) 27 | }) 28 | 29 | test_that("ChromoteSession gets and sets viewport size", { 30 | skip_if_no_chromote() 31 | skip_if_offline() 32 | 33 | page <- ChromoteSession$new(width = 400, height = 800, mobile = TRUE) 34 | withr::defer(page$close()) 35 | 36 | # viewport requires an active page 37 | p <- page$Page$loadEventFired(wait_ = FALSE) 38 | page$Page$navigate("https://example.com", wait_ = TRUE) 39 | page$wait_for(p) 40 | 41 | init_size <- list( 42 | width = 400, 43 | height = 800, 44 | zoom = page$.__enclos_env__$private$pixel_ratio, 45 | mobile = TRUE 46 | ) 47 | 48 | expect_equal( 49 | page$get_viewport_size(), 50 | init_size 51 | ) 52 | 53 | expect_equal( 54 | page$set_viewport_size(500, 900, zoom = 2, mobile = FALSE), 55 | init_size # returned invisibly 56 | ) 57 | 58 | expect_equal( 59 | page$get_viewport_size(), 60 | list( 61 | width = 500, 62 | height = 900, 63 | zoom = 2, 64 | mobile = FALSE 65 | ) 66 | ) 67 | }) 68 | 69 | test_that("ChromoteSession inherits `auto_events_enable_args` from parent", { 70 | skip_if_no_chromote() 71 | 72 | args <- list( 73 | Fetch = list(handleAuthRequests = TRUE), 74 | Network = list(maxTotalBufferSize = 1024) 75 | ) 76 | 77 | parent <- Chromote$new() 78 | for (domain in names(args)) { 79 | parent$auto_events_enable_args(domain, !!!args[[domain]]) 80 | } 81 | page <- ChromoteSession$new(parent = parent) 82 | 83 | expect_equal( 84 | page$auto_events_enable_args("Fetch"), 85 | !!args[["Fetch"]] 86 | ) 87 | 88 | expect_equal( 89 | page$auto_events_enable_args("Network"), 90 | !!args[["Network"]] 91 | ) 92 | 93 | page$auto_events_enable_args("Fetch", handleAuthRequests = FALSE) 94 | expect_equal( 95 | page$auto_events_enable_args("Fetch"), 96 | list(handleAuthRequests = FALSE) 97 | ) 98 | expect_equal( 99 | parent$auto_events_enable_args("Fetch"), 100 | !!args[["Fetch"]] 101 | ) 102 | }) 103 | 104 | test_that("ChromoteSession$new(auto_events_enable_args)", { 105 | skip_if_no_chromote() 106 | 107 | # b <- ChromoteSession$new() 108 | # ls(b, pattern = "^[A-Z]") |> 109 | # set_names() |> 110 | # lapply(\(p) if (is_function(b[[p]]$enable)) names(fn_fmls(b[[p]]$enable))) |> 111 | # purrr::compact() |> 112 | # str() 113 | 114 | args_parent <- list(DOM = list(includeWhitespace = FALSE)) 115 | args_page <- list(DOM = list(includeWhitespace = TRUE)) 116 | 117 | parent <- Chromote$new() 118 | for (domain in names(args_parent)) { 119 | parent$auto_events_enable_args(domain, !!!args_parent[[domain]]) 120 | } 121 | 122 | page <- ChromoteSession$new(parent = parent) 123 | for (domain in names(args_page)) { 124 | page$auto_events_enable_args(domain, !!!args_page[[domain]]) 125 | } 126 | 127 | expect_equal( 128 | page$auto_events_enable_args("DOM"), 129 | !!args_page[["DOM"]] 130 | ) 131 | 132 | expect_equal( 133 | page$parent$auto_events_enable_args("DOM"), 134 | !!args_parent[["DOM"]] 135 | ) 136 | 137 | # Unset local page-specific auto events args 138 | page$auto_events_enable_args("DOM", NULL) 139 | expect_equal( 140 | page$auto_events_enable_args("DOM"), 141 | !!args_parent[["DOM"]] 142 | ) 143 | }) 144 | 145 | test_that("ChromoteSession auto_events_enable_args errors", { 146 | skip_if_no_chromote() 147 | 148 | chromote_session <- ChromoteSession$new() 149 | 150 | expect_snapshot( 151 | chromote_session$auto_events_enable_args("Browser", no_enable = TRUE), 152 | error = TRUE 153 | ) 154 | 155 | expect_snapshot( 156 | chromote_session$auto_events_enable_args("Animation", bad = TRUE), 157 | error = TRUE 158 | ) 159 | 160 | expect_warning( 161 | chromote_session$auto_events_enable_args("Animation", wait_ = TRUE) 162 | ) 163 | }) 164 | 165 | test_that("ChromoteSession with deviceScaleFactor = 0", { 166 | skip_if_no_chromote() 167 | skip_if_offline() 168 | 169 | page <- ChromoteSession$new(width = 400, height = 800, mobile = TRUE) 170 | withr::defer(page$close()) 171 | 172 | # viewport requires an active page 173 | p <- page$Page$loadEventFired(wait_ = FALSE) 174 | page$Page$navigate("https://example.com", wait_ = TRUE) 175 | page$wait_for(p) 176 | 177 | init_size <- list( 178 | width = 400, 179 | height = 800, 180 | zoom = page$.__enclos_env__$private$pixel_ratio, 181 | mobile = TRUE 182 | ) 183 | 184 | expect_equal( 185 | page$get_viewport_size(), 186 | init_size 187 | ) 188 | 189 | expect_equal( 190 | page$set_viewport_size(500, 900, zoom = 0, mobile = FALSE), 191 | init_size # returned invisibly 192 | ) 193 | 194 | expect_null(page$.__enclos_env__$private$pixel_ratio) 195 | 196 | expect_equal( 197 | page$get_viewport_size(), 198 | list( 199 | width = 500, 200 | height = 900, 201 | zoom = 0, 202 | mobile = FALSE 203 | ) 204 | ) 205 | }) 206 | -------------------------------------------------------------------------------- /tests/testthat/test-default_chromote_args.R: -------------------------------------------------------------------------------- 1 | min_chrome_arg_length <- 3 + is_inside_ci() + is_windows() 2 | 3 | test_that("default args are retrieved", { 4 | expect_gte(length(default_chrome_args()), min_chrome_arg_length) 5 | }) 6 | 7 | test_that("default args can be reset", { 8 | # safety 9 | cur_args <- get_chrome_args() 10 | on.exit( 11 | { 12 | set_chrome_args(cur_args) 13 | }, 14 | add = TRUE 15 | ) 16 | 17 | reset_chrome_args() 18 | 19 | # Exists 20 | expect_gte(length(get_chrome_args()), min_chrome_arg_length) 21 | 22 | # Remove 23 | set_chrome_args(NULL) 24 | expect_equal(length(get_chrome_args()), 0) 25 | expect_gte(length(default_chrome_args()), min_chrome_arg_length) 26 | 27 | # Reset 28 | reset_chrome_args() 29 | expect_gte(length(get_chrome_args()), min_chrome_arg_length) 30 | 31 | # Remove 32 | set_chrome_args(character(0)) 33 | expect_equal(length(get_chrome_args()), 0) 34 | }) 35 | 36 | test_that("default args can be overwritten", { 37 | # safety 38 | cur_args <- get_chrome_args() 39 | on.exit( 40 | { 41 | set_chrome_args(cur_args) 42 | }, 43 | add = TRUE 44 | ) 45 | 46 | reset_chrome_args() 47 | 48 | expect_gte(length(get_chrome_args()), min_chrome_arg_length) 49 | 50 | set_chrome_args(c("hello", "goodbye")) 51 | expect_equal(length(get_chrome_args()), 2) 52 | }) 53 | 54 | test_that("type checking", { 55 | # safety 56 | cur_args <- get_chrome_args() 57 | on.exit( 58 | { 59 | set_chrome_args(cur_args) 60 | }, 61 | add = TRUE 62 | ) 63 | 64 | expect_error(set_chrome_args(NA)) 65 | expect_error(set_chrome_args(NaN)) 66 | expect_error(set_chrome_args(1:10)) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/testthat/test-manage.R: -------------------------------------------------------------------------------- 1 | skip_on_cran() 2 | 3 | test_that("with_chrome_version('system') works", { 4 | system_path <- find_chrome() 5 | skip_if_not(nzchar(system_path), "Chrome is not installed on this system.") 6 | 7 | fake_chromote_path <- tempfile("chrome") 8 | 9 | local_chromote_chrome(fake_chromote_path) 10 | expect_equal(find_chrome(), fake_chromote_path) 11 | 12 | expect_equal( 13 | with_chrome_version("system", find_chrome(), quiet = TRUE), 14 | system_path 15 | ) 16 | }) 17 | 18 | try_chromote_info <- function() { 19 | info <- chromote_info() 20 | if (!is.null(info$error)) { 21 | rlang::abort(c("Could not resolve full `chromote_info()`.", i = info$error)) 22 | } 23 | 24 | info$path <- sub(normalizePath("~/"), "~", info$path) 25 | list(path = info$path, version = info$version) 26 | } 27 | 28 | test_that("with_chrome_version() manages Chromote object", { 29 | chrome_versions_add("128.0.6612.0", "chrome") 30 | chrome_versions_add("129.0.6668.100", "chrome-headless-shell") 31 | 32 | expect_closed <- function(chromote_obj) { 33 | max_wait <- Sys.time() + 15 34 | while (chromote_obj$is_alive() && Sys.time() < max_wait) { 35 | Sys.sleep(0.1) 36 | } 37 | if (Sys.time() >= max_wait) { 38 | warning("Waited the full 15 seconds for the process to close") 39 | } 40 | expect_false(chromote_obj$is_alive()) 41 | } 42 | 43 | chromote_128 <- NULL 44 | 45 | # Another copy of chromote 128 that we start globally, should be unaffected 46 | chromote_128_global <- Chromote$new( 47 | browser = Chrome$new(path = chrome_versions_path("128.0.6612.0", "chrome")) 48 | ) 49 | 50 | with_chrome_version("128.0.6612.0", { 51 | expect_equal(find_chrome(), chrome_versions_path("128.0.6612.0")) 52 | if (!has_chromote()) { 53 | skip(sprintf( 54 | "Skipping because Chrome failed to start (%s)", 55 | find_chrome() 56 | )) 57 | } 58 | chromote_128 <- default_chromote_object() 59 | chromote_129 <- NULL 60 | 61 | with_chrome_version("129.0.6668.100", binary = "chrome-headless-shell", { 62 | expect_equal( 63 | find_chrome(), 64 | chrome_versions_path("129.0.6668.100", "chrome-headless-shell") 65 | ) 66 | if (!has_chromote()) { 67 | skip(sprintf( 68 | "Skipping because Chrome failed to start (%s)", 69 | find_chrome() 70 | )) 71 | } 72 | chromote_129 <- default_chromote_object() 73 | 74 | expect_true(chromote_129$is_alive()) 75 | expect_equal(chromote_129$get_browser()$get_path(), find_chrome()) 76 | expect_true(!identical(chromote_129, chromote_128)) 77 | }) 78 | 79 | expect_equal(default_chromote_object(), chromote_128) 80 | 81 | expect_closed(chromote_129) 82 | expect_true(chromote_128$is_alive()) 83 | }) 84 | 85 | expect_closed(chromote_128) 86 | 87 | # The global chromote 128 process is still running 88 | expect_true(chromote_128_global$is_alive()) 89 | chromote_128_global$close() 90 | expect_closed(chromote_128_global) 91 | }) 92 | 93 | test_that("with_chrome_version() works", { 94 | chrome_versions_add("128.0.6612.0", "chrome") 95 | 96 | expect_snapshot( 97 | with_chrome_version("128.0.6612.0", with_retries(try_chromote_info)), 98 | variant = guess_platform() 99 | ) 100 | 101 | with_chrome_version("128.0.6612.0", { 102 | if (!has_chromote()) { 103 | skip(sprintf( 104 | "Skipping because Chrome failed to start (%s)", 105 | find_chrome() 106 | )) 107 | } 108 | 109 | b <- ChromoteSession$new() 110 | 111 | expect_match( 112 | b$Runtime$evaluate("navigator.appVersion")$result$value, 113 | "HeadlessChrome/128" 114 | ) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | test_that("with_random_port() tries expected number of ports in range", { 2 | min <- 2000L 3 | max <- 4000L 4 | n <- 25 5 | 6 | tried_ports <- c() 7 | 8 | try_unavailable_port <- function(port) { 9 | tried_ports <<- c(tried_ports, port) 10 | stop("Port ", port, " is unavailable.") 11 | } 12 | 13 | expect_error( 14 | with_random_port( 15 | try_unavailable_port, 16 | min = min, 17 | max = max, 18 | n = n 19 | ) 20 | ) 21 | 22 | expect_length(tried_ports, n) 23 | expect_true(all(tried_ports >= min)) 24 | expect_true(all(tried_ports <= max)) 25 | }) 26 | 27 | test_that("with_random_port() stops trying for `error_stop_port_search` errors", { 28 | tried_ports <- c() 29 | 30 | try_port_with_fatal_error <- function(port) { 31 | tried_ports <<- c(tried_ports, port) 32 | rlang::abort( 33 | paste0("Port ", port, " is unavailable."), 34 | class = "error_stop_port_search" 35 | ) 36 | } 37 | 38 | expect_error( 39 | with_random_port(try_port_with_fatal_error), 40 | class = "error_stop_port_search" 41 | ) 42 | expect_length(tried_ports, 1) 43 | }) 44 | 45 | test_that("with_random_port() returns result of `startup()`", { 46 | tried_ports <- c() 47 | 48 | accept_round_port <- function(port) { 49 | if (port %% 5 == 0) { 50 | return(port) 51 | } 52 | 53 | tried_ports <<- c(tried_ports, port) 54 | stop("Odd port") 55 | } 56 | 57 | port <- with_random_port(accept_round_port, n = 100) 58 | 59 | expect_true(port %% 5 == 0) 60 | 61 | if (length(tried_ports)) { 62 | expect_true(all(tried_ports %% 5 > 0)) 63 | } 64 | }) 65 | 66 | test_that("with_random_port() startup function can return NULL", { 67 | accept_any_port <- function(port) { 68 | NULL 69 | } 70 | 71 | port <- with_random_port(accept_any_port) 72 | expect_null(port) 73 | }) 74 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/chromote.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "chromote" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{chromote} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | MAN_PATH <- "../man" 20 | ``` 21 | 22 | ```{r child="../man/fragments/features.Rmd"} 23 | ``` 24 | 25 | ```{r child="../man/fragments/install.Rmd"} 26 | ``` 27 | 28 | ```{r child="../man/fragments/basic-usage.Rmd"} 29 | ``` 30 | 31 | 32 | > **Technical Note** 33 | > 34 | > All members of `Chromote` and `ChromoteSession` objects which start with a capital letter (like `b$Page`, `b$DOM`, and `b$Browser`) correspond to domains from the Chrome DevTools Protocol, and are documented in the [official CDP site](https://chromedevtools.github.io/devtools-protocol/). 35 | > All members which start with a lower-case letter (like `b$screenshot` and `b$close`) are not part of the Chrome DevTools Protocol, and are specific to `Chromote` and `ChromoteSession`. 36 | 37 | Here is an example of how to use Chromote to find the position of a DOM element using [DOM.getBoxModel](https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getBoxModel). 38 | 39 | ``` r 40 | x <- b$DOM$getDocument() 41 | x <- b$DOM$querySelector(x$root$nodeId, ".sidebar") 42 | x <- b$DOM$getBoxModel(x$nodeId) 43 | str(x) 44 | #> List of 1 45 | #> $ model:List of 6 46 | #> ..$ content:List of 8 47 | #> .. ..$ : num 128 48 | #> .. ..$ : int 28 49 | #> .. ..$ : num 292 50 | #> .. ..$ : int 28 51 | #> .. ..$ : num 292 52 | #> .. ..$ : num 988 53 | #> .. ..$ : num 128 54 | #> .. ..$ : num 988 55 | #> ..$ padding:List of 8 56 | #> .. ..$ : num 112 57 | #> .. ..$ : int 28 58 | #> .. ..$ : num 308 59 | #> .. ..$ : int 28 60 | #> .. ..$ : num 308 61 | #> .. ..$ : num 988 62 | #> .. ..$ : num 112 63 | #> .. ..$ : num 988 64 | #> ..$ border :List of 8 65 | #> .. ..$ : num 112 66 | #> .. ..$ : int 28 67 | #> .. ..$ : num 308 68 | #> .. ..$ : int 28 69 | #> .. ..$ : num 308 70 | #> .. ..$ : num 988 71 | #> .. ..$ : num 112 72 | #> .. ..$ : num 988 73 | #> ..$ margin :List of 8 74 | #> .. ..$ : int 15 75 | #> .. ..$ : int 28 76 | #> .. ..$ : num 308 77 | #> .. ..$ : int 28 78 | #> .. ..$ : num 308 79 | #> .. ..$ : num 1030 80 | #> .. ..$ : int 15 81 | #> .. ..$ : num 1030 82 | #> ..$ width : int 195 83 | #> ..$ height : int 960 84 | ``` 85 | 86 | ## Creating new tabs and managing the process 87 | 88 | To create a new tab/window: 89 | 90 | ``` r 91 | b1 <- b$new_session() 92 | ``` 93 | 94 | Once it's created, you can perform operations with the new tab without affecting the first one. 95 | 96 | ``` r 97 | b1$view() 98 | b1$Page$navigate("https://github.com/rstudio/chromote") 99 | #> $frameId 100 | #> [1] "714439EBDD663E597658503C86F77B0B" 101 | #> 102 | #> $loaderId 103 | #> [1] "F39339CBA7D1ACB83618FEF40C3C7467" 104 | ``` 105 | 106 | To close a browser tab/window, you can run: 107 | 108 | ``` r 109 | b1$close() 110 | ``` 111 | 112 | This is different from shutting down the browser process. 113 | If you call `b$close()`, the browser process will still be running, even if all tabs have been closed. 114 | If all tabs have been closed, you can still create a new tab by calling `b1$new_session()`. 115 | 116 | To shut down the process, call: 117 | 118 | ``` r 119 | b1$parent$close() 120 | ``` 121 | 122 | `b1$parent` is a `Chromote` object (as opposed to `ChromoteSession`), which represents the browser as a whole. 123 | This is explained in [The Chromote object model](#the-chromote-object-model). 124 | 125 | ## Commands and Events 126 | 127 | The Chrome DevTools Protocol has two types of methods: *commands* and *events*. 128 | The methods used in the previous examples are commands. 129 | That is, they tell the browser to do something; the browser does it, and then sends back some data. 130 | Learn more in `vignette("commands-and-events")`. 131 | 132 | ## The Chromote object model {#the-chromote-object-model} 133 | 134 | There are two R6 classes that are used to represent the Chrome browser. 135 | One is `Chromote`, and the other is `ChromoteSession`. 136 | A `Chromote` object represents the browser as a whole, and it can have multiple *targets*, which each represent a browser tab. 137 | In the Chrome DevTools Protocol, each target can have one or more debugging *sessions* to control it. 138 | A `ChromoteSession` object represents a single *session*. 139 | 140 | When a `ChromoteSession` object is instantiated, a target is created, then a session is attached to that target, and the `ChromoteSession` object represents the session. 141 | (It is possible, though not very useful, to have multiple `ChromoteSession` objects connected to the same target, each with a different session.) 142 | 143 | A `Chromote` object can have any number of `ChromoteSession` objects as children. 144 | It is not necessary to create a `Chromote` object manually. 145 | You can simply call: 146 | 147 | ``` r 148 | b <- ChromoteSession$new() 149 | ``` 150 | 151 | and it will automatically create a `Chromote` object if one has not already been created. 152 | The Chromote package will then designate that `Chromote` object as the *default* Chromote object for the package, so that any future calls to `ChromoteSession$new()` will automatically use the same `Chromote`. 153 | This is so that it doesn't start a new browser for every `ChromoteSession` object that is created. 154 | 155 | In the Chrome DevTools Protocol, most commands can be sent to individual sessions using the `ChromoteSession` object, but there are some commands which can only be sent to the overall browser, using the `Chromote` object. 156 | 157 | To access the parent `Chromote` object from a `ChromoteSession`, you can simply use `$parent`: 158 | 159 | ``` r 160 | b <- ChromoteSession$new() 161 | m <- b$parent 162 | ``` 163 | 164 | With a `Chromote` object, you can get a list containing all the `ChromoteSession`s, with `$get_sessions()`: 165 | 166 | ``` r 167 | m$get_sessions() 168 | ``` 169 | 170 | Normally, subsequent calls to `ChromoteSession$new()` will use the existing `Chromote` object. 171 | However, if you want to start a new browser process, you can manually create a `Chromote` object, then spawn a session from it; or you can pass the new `Chromote` object to `ChromoteSession$new()`: 172 | 173 | ``` r 174 | cm <- Chromote$new() 175 | b1 <- cm$new_session() 176 | 177 | # Or: 178 | b1 <- ChromoteSession$new(parent = cm) 179 | ``` 180 | 181 | Note that if you use either of these methods, the new `Chromote` object `cm` will *not* be set as the default that is used by future calls to `ChromoteSesssion$new()`. 182 | See `vignette("which-chrome")` for an example showing how you can set the default `Chromote` object. 183 | 184 | There are also the following classes which represent the browser at a lower level: 185 | 186 | - `Browser`: This represents an instance of a browser that supports the Chrome DevTools Protocol. It contains information about how to communicate with the Chrome browser. A `Chromote` object contains one of these. 187 | - `Chrome`: This is a subclass of `Browser` that represents a local browser. It extends the `Browser` class with a `processx::process` object, which represents the browser's system process. 188 | - `ChromeRemote`: This is a subclass of `Browser` that represents a browser running on a remote system. 189 | 190 | ## Debugging 191 | 192 | Calling `b$debug_messages(TRUE)` will enable the printing of all the JSON messages sent between R and Chrome. 193 | This can be very helpful for understanding how the Chrome DevTools Protocol works. 194 | 195 | ``` r 196 | b <- ChromoteSession$new() 197 | b$parent$debug_messages(TRUE) 198 | b$Page$navigate("https://www.r-project.org/") 199 | #> SEND {"method":"Page.navigate","params":{"url":"https://www.r-project.org/"},"id":53,"sessionId":"12CB6B044A379DA0BDCFBBA55318247C"} 200 | #> $frameId 201 | #> [1] "BAAC175C67E55886207BADE1776E7B1F" 202 | #> 203 | #> $loaderId 204 | #> [1] "66DED3DF9403DA4A307444765FDE828E" 205 | 206 | # Disable debug messages 207 | b$parent$debug_messages(FALSE) 208 | ``` 209 | 210 | ## Resource cleanup and garbage collection 211 | 212 | When Chromote starts a Chrome process, it calls `Chrome$new()`. 213 | This launches the Chrome process it using `processx::process()`, and enables a supervisor for the process. 214 | This means that if the R process stops, the supervisor will detect this and shut down any Chrome processes that were registered with the supervisor. 215 | This prevents the proliferation of Chrome processes that are no longer needed. 216 | 217 | The Chromote package will, by default, use a single Chrome process and a single `Chromote` object, and each time `ChromoteSession$new()` is called, it will spawn them from the `Chromote` object. 218 | See [The Chromote object model](#the-chromote-object-model) for more information. -------------------------------------------------------------------------------- /vignettes/commands-and-events.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Commands and events" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Commands and events} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | # Commands and Events 22 | 23 | The Chrome DevTools Protocol has two types of methods: *commands* and *events*. 24 | Methods like `Page$navigate()` and `DOM$querySelector()` are **commands**. 25 | That is, they tell the browser to do something; the browser does it, and then sends back some data. 26 | 27 | **Events** are quite different from commands. 28 | When, for example, you run `b$Page$loadEventFired()`, it does not send a message to the browser. 29 | Rather, this method tells the R process to wait until it receives a `Page.loadEventFired` message from the browser. 30 | 31 | Here is an example of how that event can be used. 32 | Note that these two lines of code must be run together, without any delay at all (this can be enforced by wrapping both lines of code in `{ .... }`). 33 | 34 | ``` r 35 | library(chromote) 36 | b <- ChromoteSession$new() 37 | 38 | # Send a command to navigate to a page 39 | b$Page$navigate("https://www.r-project.org") 40 | #> $frameId 41 | #> [1] "0ADE3CFBAF764B0308ADE1ACCC33358B" 42 | #> 43 | #> $loaderId 44 | #> [1] "112AF4AC0C13FF4A95BED8173C3F4C7F" 45 | 46 | # Wait for the Page.loadEventFired event 47 | b$Page$loadEventFired() 48 | #> $timestamp 49 | #> [1] 680.7603 50 | ``` 51 | 52 | After running these two lines, the R process will be blocked. 53 | While it's blocked, the browser will load the page, and then send a message to the R process saying that the `Page.loadEventFired` event has occurred. 54 | The message looks something like this: 55 | 56 | ``` json 57 | {"method":"Page.loadEventFired","params":{"timestamp":699232.345338}} 58 | ``` 59 | 60 | After the R process receives this message, the function returns the value, which looks like this: 61 | 62 | ``` 63 | $timestamp 64 | [1] 699232.3 65 | ``` 66 | 67 | > **Note:** This sequence of commands, with `Page$navigate()` and then `Page$loadEventFired()` works interactively when you run the commands slowly, but it will not work 100% of the time. 68 | > In practice you should use `$go_to()` instead for reliable loading. 69 | > See `vignette("example-loading-page")` for more information. 70 | 71 | ## Automatic Events 72 | 73 | Chromote insulates the user from some of the details of how the CDP implements event notifications. 74 | Event notifications are not sent from the browser to the R process by default; you must first send a command to enable event notifications for a domain. 75 | For example `Page.enable` enables event notifications for the `Page` domain -- the browser will send messages for *all* `Page` events. 76 | (See the Events section in [this page](https://chromedevtools.github.io/devtools-protocol/tot/Page/)). 77 | These notifications will continue to be sent until the browser receives a `Page.disable` command. 78 | 79 | By default, Chromote hides this implementation detail. 80 | When you call `b$Page$loadEventFired()`, Chromote sends a `Page.enable` command automatically, and then waits until it receives the `Page.loadEventFired` event notification. 81 | Then it sends a `Page.disable` command. 82 | 83 | Note that in asynchronous mode, the behavior is slightly more sophisticated: it maintains a counter of how many outstanding events it is waiting for in a given domain. 84 | When that count goes from 0 to 1, it sends the `X.enable` command; when the count goes from 1 to 0, it sends the `X.disable` command. 85 | For more information, see `vignette("sync-async")`. 86 | 87 | If you need to customize the arguments used by the automatically-run `enable` command, you can use the `$auto_events_enable_args()` method of a `Chromote` or `ChromoteSession` instance, e.g. `b$auto_events_enable_args("Page", enableFileChooserOpenedEvent = TRUE)`. 88 | 89 | If you do not want automatic event enabling and disabling, then when creating the ChromoteSession object, use `ChromoteSession$new(auto_events = FALSE)`. -------------------------------------------------------------------------------- /vignettes/example-attach-existing.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Attaching to existing tabs" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Attaching to existing tabs} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | ``` r 22 | library(chromote) 23 | r <- Chromote$new() 24 | ``` 25 | 26 | When you use `ChromoteSession$new()` or `b$new_session()`, you're typically connecting to an existing browser, but creating a new tab to attach to. 27 | It's also possible to attach to an existing browser *and* and existing tab. 28 | In Chrome debugging terminology a tab is called a "Target", and there is a command to retrieve the list of current Targets: 29 | 30 | ``` r 31 | r$Target$getTargets() 32 | ``` 33 | 34 | Every target has a unique identifier string associated with it called the `targetId`; `"9DAE349A3A533718ED9E17441BA5159B"` is an example of one. 35 | 36 | Here we define a function that retrieves the ID of the first Target (tab) from a Chromote object: 37 | 38 | ``` r 39 | first_id <- function(r) { 40 | ts <- r$Target$getTargets()$targetInfos 41 | stopifnot(length(ts) > 0) 42 | r$Target$getTargets()$targetInfos[[1]]$targetId 43 | } 44 | ``` 45 | 46 | The following code shows an alert box in the first tab, whatever it is: 47 | 48 | ``` r 49 | rc <- ChromeRemote$new(host = "localhost", port = 9222) 50 | r <- Chromote$new(browser = rc) 51 | tid <- first_id(r) 52 | b <- r$new_session(targetId = tid) 53 | b$Runtime$evaluate('alert("this is the first tab")') 54 | ``` -------------------------------------------------------------------------------- /vignettes/example-authentication.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Websites that require authentication" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Websites that require authentication} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | For websites that require authentication, you can use Chromote to get screenshots by doing the following: 22 | 23 | 1. Log in interactively and navigate to the page. 24 | 2. Capture cookies from the page and save them. 25 | 3. In a later R session, load the cookies. 26 | 4. Use the cookies in Chromote and navigate to the page. 27 | 5. Take a screenshot. 28 | 29 | There are two ways to capture the cookies. 30 | 31 | ## Method 1: Manually interact with the page 32 | 33 | The first method uses the headless browser's viewer. 34 | This can be a bit inconvenient because it requires going through the entire login process, even if you have already logged in with a normal browser. 35 | 36 | First navigate to the page: 37 | 38 | ``` r 39 | library(chromote) 40 | b <- ChromoteSession$new() 41 | b$view() 42 | b$go_to("https://beta.rstudioconnect.com/content/123456/") 43 | ``` 44 | 45 | Next, log in interactively via the viewer. 46 | Once that's done, use Chromote to capture the cookies. 47 | 48 | ``` r 49 | cookies <- b$Network$getCookies() 50 | str(cookies) 51 | saveRDS(cookies, "cookies.rds") 52 | ``` 53 | 54 | After saving the cookies, you can restart R and navigate to the page, using the cookies. 55 | 56 | ``` r 57 | library(chromote) 58 | b <- ChromoteSession$new() 59 | b$view() 60 | cookies <- readRDS("cookies.rds") 61 | b$Network$setCookies(cookies = cookies$cookies) 62 | # Navigate to the app that requires a login 63 | b$go_to("https://beta.rstudioconnect.com/content/123456/") 64 | b$screenshot() 65 | ``` 66 | 67 | ## Method 2: Capture and re-use cookies 68 | 69 | The second method captures the cookies using a normal browser. 70 | This is can be more convenient because, if you are already logged in, you don't need to do it again. 71 | This requires a Chromium-based browser, and it requires running DevTools-in-DevTools on that browser. 72 | 73 | First, navigate to the page in your browser. 74 | Then press CMD-Option-I (Mac) or Ctrl-Shift-I (Windows/Linux). 75 | The developer tools panel will open. 76 | Make sure to undock the developer tools so that they are in their own window. 77 | Then press CMD-Option-I or Ctrl-Shift-I again. 78 | A second developer tools window will open. 79 | (See [this SO answer](https://stackoverflow.com/questions/12291138/how-do-you-inspect-the-web-inspector-in-chrome/12291163#12291163) for detailed instructions.) 80 | 81 | In the second developer tools window, run the following: 82 | 83 | ``` js 84 | var cookies = await Main.sendOverProtocol('Network.getCookies', {}) 85 | JSON.stringify(cookies) 86 | ``` 87 | 88 | This will return a JSON string representing the cookies for that page. 89 | For example: 90 | 91 | ``` json 92 | [{"cookies":[{"name":"AWSALB","value":"T3dNdcdnMasdf/cNn0j+JHMVkZ3RI8mitnAggd9AlPsaWJdsfoaje/OowIh0qe3dDPiHc0mSafe5jNH+1Aeinfalsd30AejBZDYwE","domain":"beta.rstudioconnect.com","path":"/","expires":1594632233.96943,"size":130,"httpOnly":false,"secure":false,"session":false}]}] 93 | ``` 94 | 95 | Copy that string to the clipboard. 96 | In your R session, you can paste it to this code, surrounded by single-quotes: 97 | 98 | ``` r 99 | cookie_json <- '[{"cookies":[{"name":"AWSALB","value":"T3dNdcdnMasdf/cNn0j+JHMVkZ3RI8mitnAggd9AlPsaWJdsfoaje/OowIh0qe3dDPiHc0mSafe5jNH+1Aeinfalsd30AejBZDYwE","domain":"beta.rstudioconnect.com","path":"/","expires":1594632233.96943,"size":130,"httpOnly":false,"secure":false,"session":false}]}]' 100 | 101 | cookies <- jsonlite::fromJSON(cookie_json, simplifyVector = FALSE)[[1]] 102 | ``` 103 | 104 | Then you can use Chromote to navigate to the page and take a screenshot. 105 | 106 | ``` r 107 | library(chromote) 108 | b <- ChromoteSession$new() 109 | 110 | b$Network$setCookies(cookies = cookies$cookies) 111 | b$go_to("https://beta.rstudioconnect.com/content/123456/") 112 | 113 | b$screenshot() 114 | ``` -------------------------------------------------------------------------------- /vignettes/example-cran-tests.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Using chromote in CRAN tests" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Using chromote in CRAN tests} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ::: lead 18 | **We do not recommend using chromote in tests that you run on CRAN.** 19 | ::: 20 | 21 | We **do recommend** that you test your package's integration with chromote, just not on CRAN. 22 | Instead, use a continuous testing service, like [GitHub Actions](https://usethis.r-lib.org/reference/github_actions.html), and include `testthat::skip_on_cran()` in tests that require Chrome or chromote. 23 | 24 | There are a number of issues with testing package functionality based on chromote on CRAN: 25 | 26 | * By default, chromote uses the system installation of Chrome, which can change frequently and without warning. 27 | 28 | * chromote's API depends entirely on Chrome, which may change or break between releases. 29 | 30 | * There is no 100% reliable way to check or test which system-installed version of Chrome is used on CRAN. 31 | While `chromote_info()` can _generally_ provide this information, we use heuristics to gather the Chrome version that do not always work. 32 | 33 | * While chromote now provides features to download and use any version of Chrome, **these features should not be used on CRAN**. 34 | For one, downloading Chrome unnecessarily consumes CRAN's limited resources. 35 | Furthermore, testing against a pinned version of Chrome won't alert you to issues with the latest version. 36 | 37 | Given these challenges, we instead recommend: 38 | 39 | 1. Using `testthat::skip_on_cran()` for tests that rely on the availability of Chrome. 40 | 41 | 2. Run tests in a CI environment, ideally on a [weekly or monthly schedule](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule). 42 | 43 | 3. Use the system version of Chrome provided by the CI environment, or use 44 | ```r 45 | local_chrome_version("latest-stable") 46 | ``` 47 | to ensure you're testing against the latest stable version of Chrome. 48 | ```r 49 | local_chrome_version("latest-stable", binary = "chrome-headless-shell") 50 | ``` 51 | is another valid choice. 52 | See `vignette("which-chrome")` for details. 53 | -------------------------------------------------------------------------------- /vignettes/example-custom-headers.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setting custom headers" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Setting custom headers} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | Currently setting custom headers requires a little extra work because it requires `Network.enable` be called before using it. 22 | In the future we'll streamline things so that it will happen automatically. 23 | 24 | ``` r 25 | library(chromote) 26 | b <- ChromoteSession$new() 27 | # Currently need to manually enable Network domain notifications. Calling 28 | # b$Network$enable() would do it, but calling it directly will bypass the 29 | # callback counting and the notifications could get automatically disabled by a 30 | # different Network event. We'll enable notifications for the Network domain by 31 | # listening for a particular event. We'll also store a callback that will 32 | # decrement the callback counter, so that we can disable notifications after. 33 | disable_network_notifications <- b$Network$responseReceived(function (msg) NULL) 34 | b$Network$setExtraHTTPHeaders(headers = list( 35 | foo = "bar", 36 | header1 = "value1" 37 | )) 38 | 39 | # Visit a web page that prints out the request headers 40 | b$go_to("http://scooterlabs.com/echo") 41 | b$screenshot(show = TRUE) 42 | 43 | 44 | # Unset extra headers. Note that `list(a=1)[0]` creates an empty _named_ list; 45 | # an empty unnamed list will cause an error because they're converted to JSON 46 | # differently. A named list becomes "{}", but an unnamed list becomes "[]". 47 | b$Network$setExtraHTTPHeaders(headers = list(a=1)[0]) 48 | 49 | # Request again 50 | b$go_to("http://scooterlabs.com/echo") 51 | b$screenshot(show = TRUE) 52 | 53 | 54 | # Disable extra headers entirely, by decrementing Network callback counter, 55 | # which will disable Network notifications. 56 | disable_network_notifications() 57 | ``` -------------------------------------------------------------------------------- /vignettes/example-custom-user-agent.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setting custom user agent" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Setting custom user agent} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | A user agent is a string of text that a browser sends to a web server to identify itself, including details about the browser type, operating system, and device. 22 | 23 | In ⁠chromote, setting the user agent allows you to simulate requests from different browsers or devices. 24 | This is useful for testing how websites behave in various environments, scraping data by mimicking real user behavior, accessing mobile versions of sites, or bypassing restrictions some websites place on certain browsers. 25 | 26 | You can see the user agent string provided by your browser, or a list of other user agents strings, by using a site like . 27 | 28 | ## Synchronous version 29 | 30 | ``` r 31 | library(chromote) 32 | b <- ChromoteSession$new() 33 | 34 | b$Network$setUserAgentOverride(userAgent = "My fake browser") 35 | 36 | b$go_to("http://scooterlabs.com/echo") 37 | b$screenshot(show = TRUE) 38 | ``` 39 | 40 | ## Asynchronous version 41 | 42 | ``` r 43 | library(chromote) 44 | b <- ChromoteSession$new() 45 | 46 | b$Network$setUserAgentOverride(userAgent = "My fake browser", wait_ = FALSE) 47 | p <- b$Page$loadEventFired(wait_ = FALSE) 48 | b$go_to("http://scooterlabs.com/echo", wait_ = FALSE) 49 | p$then(function(value) { 50 | b$screenshot(show = TRUE) 51 | }) 52 | ``` -------------------------------------------------------------------------------- /vignettes/example-extract-text.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Extracting text from a web page" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Extracting text from a web page} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | ## Using JavaScript 22 | 23 | One way to extract text from a page is to tell the browser to run JavaScript code that does it. 24 | 25 | ### Synchronous version 26 | 27 | ``` r 28 | library(chromote) 29 | b <- ChromoteSession$new() 30 | 31 | b$go_to("https://www.whatismybrowser.com/") 32 | 33 | # Run JavaScript to extract text from the page 34 | x <- b$Runtime$evaluate('document.querySelector(".corset .string-major a").innerText') 35 | x$result$value 36 | #> [1] "Chrome 75 on macOS (Mojave)" 37 | ``` 38 | 39 | ### Asynchronous version 40 | 41 | ``` r 42 | library(chromote) 43 | b <- ChromoteSession$new() 44 | 45 | p <- b$Page$loadEventFired(wait_ = FALSE) 46 | b$go_to("https://www.whatismybrowser.com/", wait_ = FALSE) 47 | p$then(function(value) { 48 | b$Runtime$evaluate( 49 | 'document.querySelector(".corset .string-major a").innerText' 50 | ) 51 | })$ 52 | then(function(value) { 53 | print(value$result$value) 54 | }) 55 | ``` 56 | 57 | ## Using Chrome DevTools Protocol commands 58 | 59 | Another way is to use CDP commands to extract content from the DOM. 60 | This does not require executing JavaScript in the browser's context, but it is also not as flexible as JavaScript. 61 | 62 | ### Synchronous version 63 | 64 | ``` r 65 | library(chromote) 66 | b <- ChromoteSession$new() 67 | 68 | b$go_to("https://www.whatismybrowser.com/") 69 | x <- b$DOM$getDocument() 70 | x <- b$DOM$querySelector(x$root$nodeId, ".corset .string-major a") 71 | b$DOM$getOuterHTML(x$nodeId) 72 | #> $outerHTML 73 | #> [1] "Chrome 75 on macOS (Mojave)" 74 | ``` 75 | 76 | ### Asynchronous version 77 | 78 | ``` r 79 | library(chromote) 80 | b <- ChromoteSession$new() 81 | 82 | b$go_to("https://www.whatismybrowser.com/", wait_ = FALSE)$ 83 | then(function(value) { 84 | b$DOM$getDocument() 85 | })$ 86 | then(function(value) { 87 | b$DOM$querySelector(value$root$nodeId, ".corset .string-major a") 88 | })$ 89 | then(function(value) { 90 | b$DOM$getOuterHTML(value$nodeId) 91 | })$ 92 | then(function(value) { 93 | print(value) 94 | }) 95 | ``` -------------------------------------------------------------------------------- /vignettes/example-loading-page.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Loading a page reliably" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Loading a page reliably} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | This document explains why you should use the convenience method `$go_to()` instead of the lower-level Chrome Devtools Protocol command `Page$navigate()`. 22 | 23 | 24 | ``` r 25 | library(chromote) 26 | b <- ChromoteSession$new() 27 | ``` 28 | 29 | In many cases, the commands `Page$navigate()` and then `$Page$loadEventFired()` will not reliably block until the page loads. 30 | For example: 31 | 32 | ``` r 33 | # Not reliable 34 | b$Page$navigate("https://www.r-project.org/") 35 | b$Page$loadEventFired() # Block until page has loaded 36 | ``` 37 | 38 | This is because the browser might successfully navigate to the page before it receives the `loadEventFired` command from R. 39 | 40 | In order to navigate to a page reliably, you must issue the `loadEventFired` command first in async mode, then issue the `navigate` command, and then wait for the `loadEventFired` promise to resolve. 41 | (If it has already resolved at this point, then the code will continue.) 42 | 43 | ``` r 44 | # Reliable method 1: for use with synchronous API 45 | p <- b$Page$loadEventFired(wait_ = FALSE) # Get the promise for the loadEventFired 46 | b$Page$navigate("https://www.r-project.org/", wait_ = FALSE) 47 | 48 | # Block until p resolves 49 | b$wait_for(p) 50 | 51 | # Add more synchronous commands here 52 | b$screenshot("browser.png") 53 | ``` 54 | 55 | The above code uses the async API to do the waiting, but then assumes that you want to write subsequent code with the synchronous API. 56 | 57 | If you want to go fully async, then instead of calling `wait_for(p)`, you would simply chain more promises from `p`, using `$then()`. 58 | 59 | ``` r 60 | # Reliable method 2: for use with asynchronous API 61 | p <- b$Page$loadEventFired(wait_ = FALSE) # Get the promise for the loadEventFired 62 | b$Page$navigate("https://www.r-project.org/", wait_ = FALSE) 63 | 64 | # Chain more async commands after the page has loaded 65 | p$then(function(value) { 66 | b$screenshot("browser.png", wait_ = FALSE) 67 | }) 68 | ``` 69 | 70 | This method of calling `Page$loadEventFired()` before `Page$navigate()` is essentially what the `$go_to()` convenience method does. It can also operate in synchronous and asynchronous modes. 71 | 72 | ``` r 73 | # Synchronous API 74 | b$go_to("https://www.r-project.org/") 75 | b$screenshot("browser.png") 76 | 77 | # Asynchronous API 78 | b$go_to("https://www.r-project.org/", wait_ = FALSE)$ 79 | then(function(value) { 80 | b$screenshot("browser.png") 81 | }) 82 | ``` 83 | 84 | 85 | The synchronous and asynchronous APIs are explained in more detail in `vignette("sync-async")`. -------------------------------------------------------------------------------- /vignettes/example-remote-hosts.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chrome on remote hosts" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Chrome on remote hosts} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | Chromote can control a browser running on a remote host. 22 | To start the browser, open a terminal on the remote host and run one of the following, depending on your platform: 23 | 24 | **Warning: Depending on how the remote machine is configured, the Chrome debug server might be accessible to anyone on the Internet. Proceed with caution.** 25 | 26 | ``` 27 | # Mac 28 | "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless \ 29 | --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 30 | 31 | # Linux 32 | google-chrome --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 33 | 34 | # Windows 35 | "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless \ 36 | --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 37 | ``` 38 | 39 | Or you can launch this process in R: 40 | 41 | ```{r eval=FALSE} 42 | library(chromote) 43 | 44 | args <- c( 45 | get_chrome_args(), 46 | "--headless", 47 | "--remote-debugging-address=0.0.0.0", 48 | "--remote-debugging-port=9222" 49 | ) 50 | 51 | p <- processx::process$new(find_chrome(), args) 52 | 53 | # To (abruptly) stop this process when you're finished with it: 54 | p$kill() 55 | ``` 56 | 57 | Then, in your local R session, create a Chromote object with the `host` and `port` (you will need to use the correct IP address). 58 | Once it's created, you can spawn a session off of it which you can control as normal: 59 | 60 | ``` r 61 | library(chromote) 62 | 63 | r <- Chromote$new( 64 | browser = ChromeRemote$new(host = "10.0.0.5", port = 9222) 65 | ) 66 | 67 | b <- r$new_session() 68 | 69 | b$Browser$getVersion() 70 | b$view() 71 | b$go_to("https://www.whatismybrowser.com/") 72 | b$screenshot("browser.png") 73 | b$screenshot("browser_string.png", selector = ".string-major") 74 | ``` 75 | 76 | When you use `$view()` on the remote browser, your local browser may block scripts for security reasons, which means that you won't be able to view the remote browser. 77 | If your local browser is Chrome, there will be a shield-shaped icon in the location bar that you can click in order to enable loading the scripts. 78 | (Note: Some browsers don't seem to work at all with the viewer.) 79 | -------------------------------------------------------------------------------- /vignettes/example-screenshot.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Taking a screenshot of a web page" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Taking a screenshot of a web page} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | ``` r 22 | library(chromote) 23 | ``` 24 | 25 | ## Taking a screenshot of a web page 26 | 27 | Take a screenshot of the viewport and display it using the [showimage](https://github.com/r-lib/showimage#readme) package. 28 | This uses Chromote's `$screenshot()` method, which wraps up many calls to the Chrome DevTools Protocol. 29 | 30 | ``` r 31 | b <- ChromoteSession$new() 32 | 33 | # ==== Synchronous version ==== 34 | # Run the next two lines together, without any delay in between. 35 | b$go_to("https://www.r-project.org/") 36 | b$screenshot(show = TRUE) # Saves to screenshot.png and displays in viewer 37 | 38 | # ==== Async version ==== 39 | b$go_to("https://www.r-project.org/", wait_ = FALSE)$ 40 | then(function(value) { 41 | b$screenshot(show = TRUE) 42 | }) 43 | ``` 44 | 45 | It is also possible to use selectors to specify what to screenshot, as well as the region ("content", "border", "padding", or "margin"). 46 | 47 | ``` r 48 | # Using CSS selectors, choosing the region, and using scaling 49 | b$screenshot("s1.png", selector = ".sidebar") 50 | b$screenshot("s2.png", selector = ".sidebar", region = "margin") 51 | b$screenshot("s3.png", selector = ".page", region = "margin", scale = 2) 52 | ``` 53 | 54 | If a vector is passed to `selector`, it will take a screenshot with a rectangle that encompasses all the DOM elements picked out by the selectors. 55 | Similarly, if a selector picks out multiple DOM elements, all of them will be in the screenshot region. 56 | 57 | ## Setting width and height of the viewport (window) 58 | 59 | The default size of a `ChromoteSession` viewport is 992 by 1323 pixels. 60 | You can set the width and height when it is created: 61 | 62 | ``` r 63 | b <- ChromoteSession$new(width = 390, height = 844) 64 | 65 | b$go_to("https://www.r-project.org/") 66 | b$screenshot("narrow.png") 67 | ``` 68 | 69 | With an existing `ChromoteSession`, you can set the size with `b$set_viewport_size()`: 70 | 71 | ``` r 72 | b$set_viewport_size(width = 1600, height = 900) 73 | b$screenshot("wide.png") 74 | ``` 75 | 76 | You can take a "Retina" (double) resolution screenshot by using `b$screenshot(scale=2)`: 77 | 78 | ``` r 79 | b$screenshot("wide-2x.png", scale = 2) 80 | ``` 81 | 82 | ## Taking a screenshot of a web page after interacting with it 83 | 84 | Headless Chrome provides a remote debugging UI which you can use to interact with the web page. 85 | The ChromoteSession's `$view()` method opens a regular browser and navigates to the remote debugging UI. 86 | 87 | ``` r 88 | b <- ChromoteSession$new() 89 | 90 | b$view() 91 | b$go_to("https://www.google.com") # Or just type the URL in the navigation bar 92 | ``` 93 | 94 | At this point, you can interact with the web page by typing in text and clicking on things. 95 | 96 | Then take a screenshot: 97 | 98 | ``` r 99 | b$screenshot() 100 | ``` 101 | 102 | ## Taking screenshots of web pages in parallel 103 | 104 | With async code, it's possible to navigate to and take screenshots of multiple websites in parallel. 105 | 106 | ``` r 107 | library(promises) 108 | library(chromote) 109 | urls <- c( 110 | "https://www.r-project.org/", 111 | "https://github.com/", 112 | "https://news.ycombinator.com/" 113 | ) 114 | 115 | screenshot_p <- function(url, filename = NULL) { 116 | if (is.null(filename)) { 117 | filename <- gsub("^.*://", "", url) 118 | filename <- gsub("/", "_", filename) 119 | filename <- gsub("\\.", "_", filename) 120 | filename <- sub("_$", "", filename) 121 | filename <- paste0(filename, ".png") 122 | } 123 | 124 | b <- ChromoteSession$new() 125 | b$go_to(url, wait_ = FALSE)$ 126 | then(function(value) { 127 | b$screenshot(filename, wait_ = FALSE) 128 | })$ 129 | then(function(value) { 130 | message(filename) 131 | })$ 132 | finally(function() { 133 | b$close() 134 | }) 135 | } 136 | 137 | # Screenshot multiple simultaneously 138 | ps <- lapply(urls, screenshot_p) 139 | pa <- promise_all(.list = ps)$then(function(value) { 140 | message("Done!") 141 | }) 142 | 143 | # Block the console until the screenshots finish (optional) 144 | cm <- default_chromote_object() 145 | cm$wait_for(pa) 146 | #> www_r-project_org.png 147 | #> github_com.png 148 | #> news_ycombinator_com.png 149 | #> Done! 150 | ``` -------------------------------------------------------------------------------- /vignettes/which-chrome.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Choosing which Chrome-based browser to use" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Choosing which Chrome-based browser to use} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | 9 | editor: 10 | markdown: 11 | wrap: sentence 12 | --- 13 | 14 | ```{r, include = FALSE} 15 | knitr::opts_chunk$set( 16 | collapse = TRUE, 17 | comment = "#>" 18 | ) 19 | ``` 20 | 21 | ## Use any version of Chrome or `chrome-headless-shell` with chromote 22 | 23 | By default, chromote uses the Chrome browser installed on your system. 24 | Modern browsers automatically and frequently update, which is convenient for you when you're browsing the Internet but can easily introduce breaking and unexpected changes in your automations. 25 | 26 | The chromote package allows you to download and use any version of Chrome or `chrome-headless-shell` available via the [Google Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/) service. 27 | 28 | To get started, call `local_chrome_version()` with a specific `version` and `binary` choice at the start of your script, before you create a new `ChromoteSession`: 29 | 30 | ```r 31 | library(chromote) 32 | 33 | local_chrome_version("latest-stable", binary = "chrome") 34 | #> ℹ Downloading `chrome` version 134.0.6998.88 for mac-arm64 35 | #> trying URL 'https://storage.googleapis.com/chrome-for-testing-public/134.0.6998.88/mac-arm64/chrome-mac-arm64.zip' 36 | #> Content type 'application/zip' length 158060459 bytes (150.7 MB) 37 | #> ================================================== 38 | #> downloaded 150.7 MB 39 | #> 40 | #> ✔ Downloading `chrome` version 134.0.6998.88 for mac-arm64 [5.3s] 41 | #> chromote will now use version 134.0.6998.88 of `chrome` for mac-arm64. 42 | 43 | b <- ChromoteSession$new() 44 | ``` 45 | 46 | By default, `local_chrome_version()` uses the latest stable version of Chrome, matching the arguments shown in the code example above. 47 | 48 | For scripts with a longer life span and to ensure reproducibility, you can specify a specific version of Chrome or `chrome-headless-shell`: 49 | 50 | ```r 51 | local_chrome_version("134.0.6998.88", binary = "chrome-headless-shell") 52 | #> chromote will now use version 134.0.6998.88 of `chrome-headless-shell` for mac-arm64. 53 | ``` 54 | 55 | If you don't already have a copy of the requested version of the binary, `local_chrome_version()` will download it for you so you'll only need to download the binary once. 56 | You can list all of the versions and binaries you've installed with `chrome_versions_list()`, or all available versions and binaries with `chrome_versions_list("all")`. 57 | 58 | ```r 59 | chrome_versions_list() 60 | #> # A tibble: 2 × 6 61 | #> version revision binary platform url path 62 | #> 63 | #> 1 134.0.6998.88 1415337 chrome mac-arm64 https://storage.googleapi… /Use… 64 | #> 2 134.0.6998.88 1415337 chrome-headless-shell mac-arm64 https://storage.googleapi… /Use… 65 | ``` 66 | 67 | > **Technincal Note: chrome-headless-shell** 68 | > 69 | > chromote runs Chrome in "headless mode", i.e. without a visual interface. 70 | > Between versions 120 and 132 of Chrome, there were, essentially, two flavors of headless mode. 71 | > 72 | > `chrome-headless-shell` is the version of Chrome's headless mode that is designed and best suited for automated testing, screenshots and printing, typically referred to as "old headless" mode. 73 | > 74 | > In most uses of chromote, `chrome-headless-shell` is an appropriate choice. 75 | > It will generally load faster and run more quickly than the alternative headless mode which uses the same version of Chrome you use when browsing, but without the UI. 76 | > After v132, old headless mode is no longer included in the Chrome binary, but the `chrome-headless-shell` binary is available from v120+. 77 | 78 | `local_chrome_version()` sets the version of Chrome for the current session or within the context of a function. 79 | For small tasks where you want to use a specific version of Chrome for a few lines of code, chromote provides a `with_chrome_version()` variant: 80 | 81 | 82 | ```r 83 | with_chrome_version("132", { 84 | # Take a screenshot with Chrome v132 85 | webshot2::webshot("https://r-project.org") 86 | }) 87 | ``` 88 | 89 | Finally, you can manage Chrome binaries directly with three helper functions: 90 | 91 | 1. `chrome_versions_add()` can be used to add a new Chrome version to the cache, without explicitly configuring chromote to use that version. 92 | 93 | 2. `chrome_versions_path()` returns the path to the Chrome binary for a given version and binary type. 94 | 95 | 3. `chrome_versions_remove()` can be used to delete copies of Chrome from the local cache. 96 | 97 | > **Note for Windows users** 98 | > 99 | > Chrome for Windows includes a `setup.exe` file that chromote runs when it extracts the Chrome zipfile. 100 | > This file is provided by Chrome and is used to set the correct permissions on the `chrome.exe` executable file. 101 | > Sometimes, running `setup.exe` returns an error, even if it works correctly. 102 | > 103 | > If you do encounter errors using a downloaded version of Chrome, use `chrome_versions_path()` to get the path to the problematic executable. 104 | > Then, try running this executable yourself with the **Run** command (from the Start menu). 105 | > This typically resolves any lingering permissions issues. 106 | 107 | 108 | ## Using a Chrome-based browser that you installed 109 | 110 | Chromote will look in specific places for the Chrome web browser, depending on platform. 111 | This is done by the `chromote:::find_chrome()` function. 112 | 113 | If you wish to use a different browser from the default, you can set the `CHROMOTE_CHROME` environment variable, either with `Sys.setenv(CHROMOTE_CHROME="/path/to/browser")`. 114 | 115 | ``` r 116 | library(chromote) 117 | Sys.setenv(CHROMOTE_CHROME = "/Applications/Chromium.app/Contents/MacOS/Chromium") 118 | 119 | b <- ChromoteSession$new() 120 | b$view() 121 | b$go_to("https://www.whatismybrowser.com/") 122 | ``` 123 | 124 | Another way is create a `Chromote` object and explicitly specify the browser, then spawn `ChromoteSession`s from it. 125 | 126 | ``` r 127 | m <- Chromote$new( 128 | browser = Chrome$new(path = "/Applications/Chromium.app/Contents/MacOS/Chromium") 129 | ) 130 | 131 | # Spawn a ChromoteSession from the Chromote object 132 | b <- m$new_session() 133 | b$go_to("https://www.whatismybrowser.com/") 134 | ``` 135 | 136 | Yet another way is to create a `Chromote` object with a specified browser, then set it as the default Chromote object. 137 | 138 | ``` r 139 | m <- Chromote$new( 140 | browser = Chrome$new(path = "/Applications/Chromium.app/Contents/MacOS/Chromium") 141 | ) 142 | 143 | # Set this Chromote object as the default. Then any 144 | # ChromoteSession$new() will be spawned from it. 145 | set_default_chromote_object(m) 146 | b <- ChromoteSession$new() 147 | b$view() 148 | b$Page$navigate("https://www.whatismybrowser.com/") 149 | ``` 150 | --------------------------------------------------------------------------------