From e0e7cf965d591debbbd6de572dbb75459aaf2e8b Mon Sep 17 00:00:00 2001
From: Hadley Wickham <hadley@posit.co>
Date: Tue, 30 Jan 2024 16:39:06 -0600
Subject: [PATCH 1/5] Drop the --disable-gpu flag (#141)

It is no longer needed as the underlying bugs have been fixed in chromium (for multiple years). This flag was dropped in puppeteer in https://github.com/puppeteer/puppeteer/pull/2908 and https://github.com/puppeteer/puppeteer/pull/4523.
---
 NEWS.md                                     | 2 ++
 R/chromote.R                                | 7 +------
 man/default_chrome_args.Rd                  | 6 +-----
 tests/testthat/test-default_chromote_args.R | 2 +-
 4 files changed, 5 insertions(+), 12 deletions(-)

diff --git a/NEWS.md b/NEWS.md
index d2e9f17..671e9e0 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,7 @@
 # chromote (development version)
 
+* `--disable-gpu` is no longer included in the default Chrome arguments.
+
 * `ChromoteSession` now records the `targetId`. This eliminates one round-trip to the browser when viewing or closing a session, and will make it possible to re-start a closed session (#94).
 
 * `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)
diff --git a/R/chromote.R b/R/chromote.R
index cbc19e4..76cb060 100644
--- a/R/chromote.R
+++ b/R/chromote.R
@@ -556,8 +556,6 @@ is_missing_linux_user <- cache_value(function() {
 #' Default chromote arguments are composed of the following values (when
 #' appropriate):
 #'
-#' * [`"--disable-gpu"`](https://peter.sh/experiments/chromium-command-line-switches/#disable-gpu)
-#'   * \verb{Disables GPU hardware acceleration. If software renderer is not in place, then the GPU process won't launch.}
 #' * [`"--no-sandbox"`](https://peter.sh/experiments/chromium-command-line-switches/#no-sandbox)
 #'   * Only added when `CI` system environment variable is set, when the
 #'     user on a Linux system is not set, or when executing inside a Docker container.
@@ -584,9 +582,6 @@ is_missing_linux_user <- cache_value(function() {
 #' @export
 default_chrome_args <- function() {
   c(
-    # Better cross platform support
-    "--disable-gpu",
-
     # > Note: --no-sandbox is not needed if you properly setup a user in the container.
     # https://developers.google.com/web/updates/2017/04/headless-chrome
     if (is_inside_ci() || is_missing_linux_user() || is_inside_docker()) {
@@ -638,7 +633,7 @@ reset_chrome_args <- function() {
 #' @examples
 #' old_chrome_args <- get_chrome_args()
 #'
-#' # Only disable the gpu and using `/dev/shm`
+#' # Disable the gpu and use `/dev/shm`
 #' set_chrome_args(c("--disable-gpu", "--disable-dev-shm-usage"))
 #'
 #' #... Make new `Chrome` or `ChromoteSession` instance
diff --git a/man/default_chrome_args.Rd b/man/default_chrome_args.Rd
index 241d803..a50c451 100644
--- a/man/default_chrome_args.Rd
+++ b/man/default_chrome_args.Rd
@@ -32,10 +32,6 @@ list of possible arguments.
 Default chromote arguments are composed of the following values (when
 appropriate):
 \itemize{
-\item \href{https://peter.sh/experiments/chromium-command-line-switches/#disable-gpu}{\code{"--disable-gpu"}}
-\itemize{
-\item \verb{Disables GPU hardware acceleration. If software renderer is not in place, then the GPU process won't launch.}
-}
 \item \href{https://peter.sh/experiments/chromium-command-line-switches/#no-sandbox}{\code{"--no-sandbox"}}
 \itemize{
 \item Only added when \code{CI} system environment variable is set, when the
@@ -82,7 +78,7 @@ passed when initializing. Returns the updated defaults.
 \examples{
 old_chrome_args <- get_chrome_args()
 
-# Only disable the gpu and using `/dev/shm`
+# Disable the gpu and use `/dev/shm`
 set_chrome_args(c("--disable-gpu", "--disable-dev-shm-usage"))
 
 #... Make new `Chrome` or `ChromoteSession` instance
diff --git a/tests/testthat/test-default_chromote_args.R b/tests/testthat/test-default_chromote_args.R
index b8e1cb9..0ac879a 100644
--- a/tests/testthat/test-default_chromote_args.R
+++ b/tests/testthat/test-default_chromote_args.R
@@ -1,5 +1,5 @@
 
-min_chrome_arg_length <- if (is_inside_ci()) 5 else 4
+min_chrome_arg_length <- if (is_inside_ci()) 4 else 3
 
 test_that("default args are retrieved", {
   expect_gte(length(default_chrome_args()), min_chrome_arg_length)

From ff76fde5b637e6036a10e30b2417c7d340d07984 Mon Sep 17 00:00:00 2001
From: Hadley Wickham <hadley@posit.co>
Date: Tue, 30 Jan 2024 16:40:01 -0600
Subject: [PATCH 2/5] Minor documentation improvements (#138)

---
 R/chromote_session.R   | 10 +---------
 man/ChromoteSession.Rd |  6 +-----
 2 files changed, 2 insertions(+), 14 deletions(-)

diff --git a/R/chromote_session.R b/R/chromote_session.R
index f6b8391..0a1b52b 100644
--- a/R/chromote_session.R
+++ b/R/chromote_session.R
@@ -9,15 +9,6 @@
 #' but this is not currently supported by chromote.
 #'
 #' @export
-#' @param timeout_ Number of seconds for \pkg{chromote} to wait for a Chrome
-#' DevTools Protocol response. If `timeout_` is [`rlang::missing_arg()`] and
-#' `timeout` is provided, `timeout_` will be set to `2 * timeout / 1000`.
-#' @param timeout Number of milliseconds for Chrome DevTools Protocol execute a
-#' method.
-#' @param width Width, in pixels, of the `Target` to create if `targetId` is
-#'   `NULL`
-#' @param height Height, in pixels, of the `Target` to create if `targetId` is
-#'   `NULL`
 #' @param targetId
 #'   [Target](https://chromedevtools.github.io/devtools-protocol/tot/Target/)
 #'   ID of an existing target to attach to. When a `targetId` is provided, the
@@ -53,6 +44,7 @@ ChromoteSession <- R6Class(
     #'   from the parent `Chromote` object. If `TRUE`, enable automatic
     #'   event enabling/disabling; if `FALSE`, disable automatic event
     #'   enabling/disabling.
+    #' @param width,height Width and height of the new window.
     #' @param wait_ If `FALSE`, return a [promises::promise()] of a new
     #'   `ChromoteSession` object. Otherwise, block during initialization, and
     #'   return a `ChromoteSession` object directly.
diff --git a/man/ChromoteSession.Rd b/man/ChromoteSession.Rd
index ba50da8..84ab906 100644
--- a/man/ChromoteSession.Rd
+++ b/man/ChromoteSession.Rd
@@ -82,11 +82,7 @@ if (interactive()) b$view()
 \item{\code{parent}}{\code{\link{Chromote}} object to use; defaults to
 \code{\link[=default_chromote_object]{default_chromote_object()}}}
 
-\item{\code{width}}{Width, in pixels, of the \code{Target} to create if \code{targetId} is
-\code{NULL}}
-
-\item{\code{height}}{Height, in pixels, of the \code{Target} to create if \code{targetId} is
-\code{NULL}}
+\item{\code{width, height}}{Width and height of the new window.}
 
 \item{\code{targetId}}{\href{https://chromedevtools.github.io/devtools-protocol/tot/Target/}{Target}
 ID of an existing target to attach to. When a \code{targetId} is provided, the

From c4415ff477a2e436b69e38ee96888f0e594f17e2 Mon Sep 17 00:00:00 2001
From: Hadley Wickham <hadley@posit.co>
Date: Wed, 31 Jan 2024 07:12:40 -0600
Subject: [PATCH 3/5] Extract out connection code into own method (#137)

Part of #94
---
 R/chromote.R    | 57 +++++++++++++++++++++++++++++++------------------
 man/Chromote.Rd | 23 ++++++++++++++++++++
 2 files changed, 59 insertions(+), 21 deletions(-)

diff --git a/R/chromote.R b/R/chromote.R
index 76cb060..a076108 100644
--- a/R/chromote.R
+++ b/R/chromote.R
@@ -38,17 +38,42 @@ Chromote <- R6Class(
       private$auto_events   <- auto_events
       private$multi_session <- multi_session
 
+      private$command_callbacks <- fastmap()
+
+      # Use a private event loop to drive the websocket
+      private$child_loop <- create_loop(parent = current_loop())
+
+      p <- self$connect(multi_session = multi_session, wait_ = FALSE)
+
+      # Populate methods while the connection is being established.
+      protocol_spec <- jsonlite::fromJSON(self$url("/json/protocol"), simplifyVector = FALSE)
+      self$protocol <- process_protocol(protocol_spec, self$.__enclos_env__)
+      lockBinding("protocol", self)
+      # self$protocol is a list of domains, each of which is a list of
+      # methods. Graft the entries from self$protocol onto self
+      list2env(self$protocol, self)
+
+      private$event_manager <- EventManager$new(self)
+      private$is_active_ <- TRUE
+
+      self$wait_for(p)
+
+      private$register_default_event_listeners()
+    },
+
+    #' @description Re-connect the websocket to the browser. The Chrome browser
+    #'   automatically closes websockets when your computer goes to sleep;
+    #'   you can use this to bring it back to life with a new connection.
+    #' @param multi_session Should multiple sessions be allowed?
+    #' @param wait_ If `FALSE`, return a promise; if `TRUE` wait until
+    #'   connection is complete.
+    connect = function(multi_session = TRUE, wait_ = TRUE) {
       if (multi_session) {
         chrome_info <- fromJSON(self$url("/json/version"))
       } else {
         chrome_info <- fromJSON(self$url("/json"))
       }
 
-      private$command_callbacks <- fastmap()
-
-      # Use a private event loop to drive the websocket
-      private$child_loop <- create_loop(parent = current_loop())
-
       with_loop(private$child_loop, {
         private$ws <- WebSocket$new(
           chrome_info$webSocketDebuggerUrl,
@@ -81,23 +106,13 @@ Chromote <- R6Class(
           })
 
         private$ws$connect()
-
-        # Populate methods while the connection is being established.
-        protocol_spec <- jsonlite::fromJSON(self$url("/json/protocol"), simplifyVector = FALSE)
-        self$protocol <- process_protocol(protocol_spec, self$.__enclos_env__)
-        lockBinding("protocol", self)
-
-        # self$protocol is a list of domains, each of which is a list of
-        # methods. Graft the entries from self$protocol onto self
-        list2env(self$protocol, self)
-
-        private$event_manager <- EventManager$new(self)
-        private$is_active_ <- TRUE
-
-        self$wait_for(p)
-
-        private$register_default_event_listeners()
       })
+
+      if (wait_) {
+        invisible(self$wait_for(p))
+      } else {
+        p
+      }
     },
 
     #' @description Display the current session in the `browser`
diff --git a/man/Chromote.Rd b/man/Chromote.Rd
index 9594cdc..cd6e31f 100644
--- a/man/Chromote.Rd
+++ b/man/Chromote.Rd
@@ -44,6 +44,7 @@ wait for a Chrome DevTools Protocol response.}
 \subsection{Public methods}{
 \itemize{
 \item \href{#method-Chromote-new}{\code{Chromote$new()}}
+\item \href{#method-Chromote-connect}{\code{Chromote$connect()}}
 \item \href{#method-Chromote-view}{\code{Chromote$view()}}
 \item \href{#method-Chromote-get_auto_events}{\code{Chromote$get_auto_events()}}
 \item \href{#method-Chromote-get_child_loop}{\code{Chromote$get_child_loop()}}
@@ -83,6 +84,28 @@ if \code{FALSE}, disable automatic event enabling/disabling.}
 }
 }
 \if{html}{\out{<hr>}}
+\if{html}{\out{<a id="method-Chromote-connect"></a>}}
+\if{latex}{\out{\hypertarget{method-Chromote-connect}{}}}
+\subsection{Method \code{connect()}}{
+Re-connect the websocket to the browser. The Chrome browser
+automatically closes websockets when your computer goes to sleep;
+you can use this to bring it back to life with a new connection.
+\subsection{Usage}{
+\if{html}{\out{<div class="r">}}\preformatted{Chromote$connect(multi_session = TRUE, wait_ = TRUE)}\if{html}{\out{</div>}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{<div class="arguments">}}
+\describe{
+\item{\code{multi_session}}{Should multiple sessions be allowed?}
+
+\item{\code{wait_}}{If \code{FALSE}, return a promise; if \code{TRUE} wait until
+connection is complete.}
+}
+\if{html}{\out{</div>}}
+}
+}
+\if{html}{\out{<hr>}}
 \if{html}{\out{<a id="method-Chromote-view"></a>}}
 \if{latex}{\out{\hypertarget{method-Chromote-view}{}}}
 \subsection{Method \code{view()}}{

From af5dbe7d9561094e7cebc9259b37e13ac9f8d947 Mon Sep 17 00:00:00 2001
From: Hadley Wickham <hadley@posit.co>
Date: Wed, 31 Jan 2024 07:24:15 -0600
Subject: [PATCH 4/5] Implement `ChromoteSession$respawn()` & refactor session
 creation (#139)

Pull out separate `create_session()` function to make it more clear that this is a factory class method.

Part of #94
---
 NEWS.md                                |  2 +-
 R/chromote.R                           | 20 ++++------
 R/chromote_session.R                   | 55 ++++++++++++++++++++++++--
 man/ChromoteSession.Rd                 | 23 ++++++++---
 tests/testthat/helper.R                | 27 +++++++++++++
 tests/testthat/test-chromote_session.R |  9 +++++
 6 files changed, 114 insertions(+), 22 deletions(-)
 create mode 100644 tests/testthat/helper.R
 create mode 100644 tests/testthat/test-chromote_session.R

diff --git a/NEWS.md b/NEWS.md
index 671e9e0..25a54d5 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,7 +2,7 @@
 
 * `--disable-gpu` is no longer included in the default Chrome arguments.
 
-* `ChromoteSession` now records the `targetId`. This eliminates one round-trip to the browser when viewing or closing a session, and will make it possible to re-start a closed session (#94).
+* `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).
 
 * `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)
 
diff --git a/R/chromote.R b/R/chromote.R
index a076108..5b19392 100644
--- a/R/chromote.R
+++ b/R/chromote.R
@@ -175,19 +175,13 @@ Chromote <- R6Class(
     #'   `ChromoteSession` object. Otherwise, block during initialization, and
     #'   return a `ChromoteSession` object directly.
     new_session = function(width = 992, height = 1323, targetId = NULL, wait_ = TRUE) {
-      session <- ChromoteSession$new(self, width, height, targetId, wait_ = FALSE)
-
-      # ChromoteSession$new() always returns the object, but the
-      # initialization is async. To properly wait for initialization, we
-      # need to call b$init_promise() to get the promise; it resolves
-      # after initialization is complete.
-      p <- session$init_promise()
-
-      if (wait_) {
-        self$wait_for(p)
-      } else {
-        p
-      }
+      create_session(
+        chromote = self,
+        width = width,
+        height = height,
+        targetId = targetId,
+        wait_ = wait_
+      )
     },
 
     #' @description Retrieve all [`ChromoteSession`] objects
diff --git a/R/chromote_session.R b/R/chromote_session.R
index 0a1b52b..6ccbc4a 100644
--- a/R/chromote_session.R
+++ b/R/chromote_session.R
@@ -129,7 +129,7 @@ ChromoteSession <- R6Class(
         # returning p, because the call to ChromoteSession$new() always
         # returns the new object. Instead, we'll store it as
         # private$init_promise_, and the user can retrieve it with
-        # b$init_promise().
+        # b$get_init_promise().
         private$init_promise_ <- p$then(function(value) self)
       }
 
@@ -409,7 +409,13 @@ ChromoteSession <- R6Class(
     #' when the `ChromoteSession` has created a new session. Otherwise, block
     #' until the `ChromoteSession` has created a new session.
     new_session = function(width = 992, height = 1323, targetId = NULL, wait_ = TRUE) {
-      self$parent$new_session(width = width, height = height, targetId = targetId, wait_ = wait_)
+      create_session(
+        chromote = self$parent,
+        width = width,
+        height = height,
+        targetId = targetId,
+        wait_ = wait_
+      )
     },
 
     #' @description
@@ -418,6 +424,22 @@ ChromoteSession <- R6Class(
       private$session_id
     },
 
+    #' @description
+    #' Create a new session that connects to the same target (i.e. page)
+    #' as this session. This is useful if the session has been closed but the target still
+    #' exists.
+    respawn = function() {
+      if (!private$is_active_) {
+        stop("Can't respawn session; target has been closed.")
+      }
+
+      create_session(
+        chromote = self$parent,
+        targetId = private$target_id,
+        auto_events = private$auto_events
+      )
+    },
+
     #' @description
     #' Retrieve the target id
     get_target_id = function() {
@@ -531,7 +553,7 @@ ChromoteSession <- R6Class(
     #' @description Initial promise
     #'
     #' For internal use only.
-    init_promise = function() {
+    get_init_promise = function() {
       private$init_promise_
     },
 
@@ -561,3 +583,30 @@ ChromoteSession <- R6Class(
     }
   )
 )
+
+
+# Wrapper around ChromoteSession$new() that can return a promise
+create_session <- function(chromote = default_chromote_object(),
+                           width = 992,
+                           height = 1323,
+                           targetId = NULL,
+                           wait_ = TRUE,
+                           auto_events = NULL) {
+
+  session <- ChromoteSession$new(
+    parent = chromote,
+    width = width,
+    height = height,
+    targetId,
+    auto_events = auto_events,
+    wait_ = wait_
+  )
+
+  if (wait_) {
+    session
+  } else {
+    # ChromoteSession$new() must return a ChromoteSession object so we need a
+    # side-channel to return a promise
+    session$get_init_promise()
+  }
+}
diff --git a/man/ChromoteSession.Rd b/man/ChromoteSession.Rd
index 84ab906..a2081f2 100644
--- a/man/ChromoteSession.Rd
+++ b/man/ChromoteSession.Rd
@@ -33,6 +33,7 @@ wait for a Chrome DevTools Protocol response.}
 \item \href{#method-ChromoteSession-screenshot_pdf}{\code{ChromoteSession$screenshot_pdf()}}
 \item \href{#method-ChromoteSession-new_session}{\code{ChromoteSession$new_session()}}
 \item \href{#method-ChromoteSession-get_session_id}{\code{ChromoteSession$get_session_id()}}
+\item \href{#method-ChromoteSession-respawn}{\code{ChromoteSession$respawn()}}
 \item \href{#method-ChromoteSession-get_target_id}{\code{ChromoteSession$get_target_id()}}
 \item \href{#method-ChromoteSession-wait_for}{\code{ChromoteSession$wait_for()}}
 \item \href{#method-ChromoteSession-debug_log}{\code{ChromoteSession$debug_log()}}
@@ -42,7 +43,7 @@ wait for a Chrome DevTools Protocol response.}
 \item \href{#method-ChromoteSession-invoke_event_callbacks}{\code{ChromoteSession$invoke_event_callbacks()}}
 \item \href{#method-ChromoteSession-mark_closed}{\code{ChromoteSession$mark_closed()}}
 \item \href{#method-ChromoteSession-is_active}{\code{ChromoteSession$is_active()}}
-\item \href{#method-ChromoteSession-init_promise}{\code{ChromoteSession$init_promise()}}
+\item \href{#method-ChromoteSession-get_init_promise}{\code{ChromoteSession$get_init_promise()}}
 }
 }
 \if{html}{\out{<hr>}}
@@ -412,6 +413,18 @@ Retrieve the session id
 \if{html}{\out{<div class="r">}}\preformatted{ChromoteSession$get_session_id()}\if{html}{\out{</div>}}
 }
 
+}
+\if{html}{\out{<hr>}}
+\if{html}{\out{<a id="method-ChromoteSession-respawn"></a>}}
+\if{latex}{\out{\hypertarget{method-ChromoteSession-respawn}{}}}
+\subsection{Method \code{respawn()}}{
+Create a new session that connects to the same target (i.e. page)
+as this session. This is useful if the session has been closed but the target still
+exists.
+\subsection{Usage}{
+\if{html}{\out{<div class="r">}}\preformatted{ChromoteSession$respawn()}\if{html}{\out{</div>}}
+}
+
 }
 \if{html}{\out{<hr>}}
 \if{html}{\out{<a id="method-ChromoteSession-get_target_id"></a>}}
@@ -583,14 +596,14 @@ called, this value will be \code{FALSE}.
 
 }
 \if{html}{\out{<hr>}}
-\if{html}{\out{<a id="method-ChromoteSession-init_promise"></a>}}
-\if{latex}{\out{\hypertarget{method-ChromoteSession-init_promise}{}}}
-\subsection{Method \code{init_promise()}}{
+\if{html}{\out{<a id="method-ChromoteSession-get_init_promise"></a>}}
+\if{latex}{\out{\hypertarget{method-ChromoteSession-get_init_promise}{}}}
+\subsection{Method \code{get_init_promise()}}{
 Initial promise
 
 For internal use only.
 \subsection{Usage}{
-\if{html}{\out{<div class="r">}}\preformatted{ChromoteSession$init_promise()}\if{html}{\out{</div>}}
+\if{html}{\out{<div class="r">}}\preformatted{ChromoteSession$get_init_promise()}\if{html}{\out{</div>}}
 }
 
 }
diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R
new file mode 100644
index 0000000..2381a31
--- /dev/null
+++ b/tests/testthat/helper.R
@@ -0,0 +1,27 @@
+skip_if_no_chromote <- function() {
+  skip_on_cran()
+  skip_on_os("windows") # currently hangs the test process
+  skip_if(lacks_chromote(), "chromote not available")
+}
+
+lacks_chromote <- function() {
+  # We try twice because in particular Windows on GHA seems to need it,
+  # but it doesn't otherwise hurt. More details at
+  # https://github.com/rstudio/shinytest2/issues/209
+  env_cache(globals, "lacks_chromote", !has_chromote() && !has_chromote())
+}
+
+has_chromote <- function() {
+  tryCatch(
+    {
+      default <- default_chromote_object()
+      local_bindings(default_timeout = 5, .env = default)
+      startup <- default$new_session(wait_ = FALSE)
+      default$wait_for(startup)
+      TRUE
+    },
+    error = function(cnd) {
+      FALSE
+    }
+  )
+}
diff --git a/tests/testthat/test-chromote_session.R b/tests/testthat/test-chromote_session.R
new file mode 100644
index 0000000..a58ad78
--- /dev/null
+++ b/tests/testthat/test-chromote_session.R
@@ -0,0 +1,9 @@
+test_that("respawning preserves targetId and auto_events", {
+  skip_if_no_chromote()
+
+  sess1 <- create_session(auto_events = FALSE)
+  sess2 <- sess1$respawn()
+
+  expect_equal(sess1$get_target_id(), sess2$get_target_id())
+  expect_equal(sess1$get_auto_events(), sess2$get_auto_events())
+})

From 454e1947a6d6387567ca4490022aa0de2c3c8125 Mon Sep 17 00:00:00 2001
From: olivroy <52606734+olivroy@users.noreply.github.com>
Date: Wed, 31 Jan 2024 09:23:11 -0500
Subject: [PATCH 5/5] Correct docs for R6 classes (#123)

---
 R/browser.R         | 2 ++
 R/chrome.R          | 4 +++-
 R/chromote.R        | 3 +--
 man/Browser.Rd      | 8 ++------
 man/Chrome.Rd       | 5 -----
 man/ChromeRemote.Rd | 2 --
 man/Chromote.Rd     | 7 -------
 7 files changed, 8 insertions(+), 23 deletions(-)

diff --git a/R/browser.R b/R/browser.R
index c6a793c..a32a1c9 100644
--- a/R/browser.R
+++ b/R/browser.R
@@ -2,10 +2,12 @@ globals <- new.env()
 
 #' Browser base class
 #'
+#' @description
 #' Base class for browsers like Chrome, Chromium, etc. Defines the interface
 #' used by various browser implementations. It can represent a local browser
 #' process or one running remotely.
 #'
+#' @details
 #' The \code{initialize()} method of an implementation should set private$host
 #' and private$port. If the process is local, the \code{initialize()} method
 #' should also set private$process.
diff --git a/R/chrome.R b/R/chrome.R
index f7a82fa..b7643a2 100644
--- a/R/chrome.R
+++ b/R/chrome.R
@@ -1,5 +1,6 @@
 #' Local Chrome process
 #'
+#' @description
 #' 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.
@@ -231,7 +232,8 @@ launch_chrome_impl <- function(path, args, port) {
 
 #' Remote Chrome process
 #'
-#'
+#' @description
+#' Remote Chrome process
 #'
 #' @export
 ChromeRemote <- R6Class("ChromeRemote",
diff --git a/R/chromote.R b/R/chromote.R
index 5b19392..de2ac85 100644
--- a/R/chromote.R
+++ b/R/chromote.R
@@ -1,7 +1,6 @@
 #' Chromote class
 #'
-#' This class represents the browser as a whole.
-#'
+#' @description
 #' A `Chromote` object represents the browser as a whole, and it can have
 #' multiple _targets_, which each represent a browser tab. In the Chrome
 #' DevTools Protocol, each target can have one or more debugging _sessions_ to
diff --git a/man/Browser.Rd b/man/Browser.Rd
index 2354fbb..1f6338c 100644
--- a/man/Browser.Rd
+++ b/man/Browser.Rd
@@ -4,15 +4,11 @@
 \alias{Browser}
 \title{Browser base class}
 \description{
-Browser base class
-
-Browser base class
-}
-\details{
 Base class for browsers like Chrome, Chromium, etc. Defines the interface
 used by various browser implementations. It can represent a local browser
 process or one running remotely.
-
+}
+\details{
 The \code{initialize()} method of an implementation should set private$host
 and private$port. If the process is local, the \code{initialize()} method
 should also set private$process.
diff --git a/man/Chrome.Rd b/man/Chrome.Rd
index dc312b4..f3a5180 100644
--- a/man/Chrome.Rd
+++ b/man/Chrome.Rd
@@ -4,11 +4,6 @@
 \alias{Chrome}
 \title{Local Chrome process}
 \description{
-Local Chrome process
-
-Local Chrome process
-}
-\details{
 This is a subclass of \code{\link{Browser}} that represents a local browser. It extends
 the \code{\link{Browser}} class with a \code{\link[processx:process]{processx::process}} object, which represents
 the browser's system process.
diff --git a/man/ChromeRemote.Rd b/man/ChromeRemote.Rd
index 683ca01..0d5cc49 100644
--- a/man/ChromeRemote.Rd
+++ b/man/ChromeRemote.Rd
@@ -4,8 +4,6 @@
 \alias{ChromeRemote}
 \title{Remote Chrome process}
 \description{
-Remote Chrome process
-
 Remote Chrome process
 }
 \section{Super class}{
diff --git a/man/Chromote.Rd b/man/Chromote.Rd
index cd6e31f..66b8a8a 100644
--- a/man/Chromote.Rd
+++ b/man/Chromote.Rd
@@ -4,13 +4,6 @@
 \alias{Chromote}
 \title{Chromote class}
 \description{
-Chromote class
-
-Chromote class
-}
-\details{
-This class represents the browser as a whole.
-
 A \code{Chromote} object represents the browser as a whole, and it can have
 multiple \emph{targets}, which each represent a browser tab. In the Chrome
 DevTools Protocol, each target can have one or more debugging \emph{sessions} to