Skip to content

Commit 0e61020

Browse files
committed
Better promise handling.
1 parent 0729f56 commit 0e61020

3 files changed

Lines changed: 178 additions & 72 deletions

File tree

R/live.R

Lines changed: 128 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,8 @@ LiveHTML <- R6::R6Class(
8686
self$session <- chromote::ChromoteSession$new()
8787

8888
self$session$Network$setUserAgentOverride("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36")
89-
90-
self$session$Page$loadEventFired(callback_ = private$refresh_root)
91-
# https://github.com/rstudio/chromote/issues/102
92-
p <- self$session$Page$loadEventFired(wait_ = FALSE)
93-
self$session$Page$navigate(url, wait_ = FALSE)
94-
self$session$wait_for(p)
89+
self$session$go_to(url)
90+
private$refresh_root()
9591
},
9692

9793
#' @description Called when `print()`ed
@@ -124,54 +120,53 @@ LiveHTML <- R6::R6Class(
124120
xml2::xml_children(xml2::xml_children(xml2::read_html(html)))
125121
},
126122

127-
128123
#' @description Simulate a click on an HTML element.
129124
#' @param css CSS selector or xpath expression.
130125
#' @param n_clicks Number of clicks
131-
click = function(css, n_clicks = 1) {
126+
#' @param wait_for What to wait for after the click. Can be `NULL` (the
127+
#' default, waits only for the click event to be sent), `"load"` (waits
128+
#' for a `Page.loadEventFired` event), or a custom promise.
129+
click = function(css, n_clicks = 1, wait_for = NULL) {
132130
private$check_active()
133131
check_number_whole(n_clicks, min = 1)
134132

135-
# Implementation based on puppeteer as described in
136-
# https://medium.com/@aslushnikov/automating-clicks-in-chromium-a50e7f01d3fb
137-
# With code from https://github.com/puppeteer/puppeteer/blob/b53de4e0942e93c/packages/puppeteer-core/src/cdp/Input.ts#L431-L459
138-
139133
node <- private$wait_for_selector(css)
140134
self$session$DOM$scrollIntoViewIfNeeded(node)
141135

142-
# Quad = location of four corners (x1, y1, x2, y2, x3, y3, x4, y4)
143-
# Relative to viewport
144136
quads <- self$session$DOM$getBoxModel(node)
145137
content_quad <- as.numeric(quads$model$content)
146138
center_x <- mean(content_quad[c(1, 3, 5, 7)])
147139
center_y <- mean(content_quad[c(2, 4, 6, 8)])
148140

149-
private$loadEvent_promise <- self$session$Page$loadEventFired(wait_ = FALSE)
141+
p_wait <- private$expand_promise(wait_for)
142+
is_async <- !is.null(p_wait)
150143

151-
# https://chromedevtools.github.io/devtools-protocol/1-3/Input/#method-dispatchMouseEvent
152144
self$session$Input$dispatchMouseEvent(
153145
type = "mouseMoved",
154146
x = center_x,
155147
y = center_y,
148+
wait_ = !is_async
156149
)
157-
158150
for (i in seq_len(n_clicks)) {
159151
self$session$Input$dispatchMouseEvent(
160152
type = "mousePressed",
161153
x = center_x,
162154
y = center_y,
163155
button = "left",
164156
clickCount = i,
157+
wait_ = !is_async
165158
)
166159
self$session$Input$dispatchMouseEvent(
167160
type = "mouseReleased",
168161
x = center_x,
169162
y = center_y,
170163
clickCount = i,
171-
button = "left"
164+
button = "left",
165+
wait_ = !is_async
172166
)
173167
}
174168

169+
self$wait_for(p_wait)
175170
invisible(self)
176171
},
177172

@@ -187,37 +182,48 @@ LiveHTML <- R6::R6Class(
187182

188183
#' @description Scroll selected element into view.
189184
#' @param css CSS selector or xpath expression.
190-
scroll_into_view = function(css) {
185+
#' @param wait_for What to wait for after the click. Can be `NULL` (the
186+
#' default, waits only for the click event to be sent), `"load"` (waits
187+
#' for a `Page.loadEventFired` event), or a custom promise.
188+
scroll_into_view = function(css, wait_for = NULL) {
191189
private$check_active()
192190
node <- private$wait_for_selector(css)
191+
p_wait <- private$expand_promise(wait_for)
193192
self$session$DOM$scrollIntoViewIfNeeded(node)
194-
193+
self$wait_for(p_wait)
195194
invisible(self)
196195
},
197196

198197
#' @description Scroll to specified location
199198
#' @param top,left Number of pixels from top/left respectively.
200-
scroll_to = function(top = 0, left = 0) {
199+
#' @param wait_for What to wait for after the click. Can be `NULL` (the
200+
#' default, waits only for the click event to be sent), `"load"` (waits
201+
#' for a `Page.loadEventFired` event), or a custom promise.
202+
scroll_to = function(top = 0, left = 0, wait_for = NULL) {
201203
private$check_active()
202204
check_number_whole(top)
203205
check_number_whole(left)
204-
206+
p_wait <- private$expand_promise(wait_for)
205207
# https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo
206208
private$call_node_method(
207209
private$root_id,
208210
paste0(".documentElement.scrollTo(", left, ", ", top, ")")
209211
)
212+
self$wait_for(p_wait)
210213
invisible(self)
211214
},
212215

213216
#' @description Scroll by the specified amount
214217
#' @param top,left Number of pixels to scroll up/down and left/right
215218
#' respectively.
216-
scroll_by = function(top = 0, left = 0) {
219+
#' @param wait_for What to wait for after the click. Can be `NULL` (the
220+
#' default, waits only for the click event to be sent), `"load"` (waits
221+
#' for a `Page.loadEventFired` event), or a custom promise.
222+
scroll_by = function(top = 0, left = 0, wait_for = NULL) {
217223
private$check_active()
218224
check_number_whole(top)
219225
check_number_whole(left)
220-
226+
p_wait <- private$expand_promise(wait_for)
221227
# https://chromedevtools.github.io/devtools-protocol/1-3/Input/#method-dispatchMouseEvent
222228
self$session$Input$dispatchMouseEvent(
223229
type = "mouseWheel",
@@ -226,21 +232,24 @@ LiveHTML <- R6::R6Class(
226232
deltaX = left,
227233
deltaY = top
228234
)
229-
235+
self$wait_for(p_wait)
230236
invisible(self)
231237
},
232238

233239
#' @description Type text in the selected element
234240
#' @param css CSS selector or xpath expression.
235241
#' @param text A single string containing the text to type.
236-
type = function(css, text) {
242+
#' @param wait_for What to wait for after the click. Can be `NULL` (the
243+
#' default, waits only for the click event to be sent), `"load"` (waits
244+
#' for a `Page.loadEventFired` event), or a custom promise.
245+
type = function(css, text, wait_for = NULL) {
237246
private$check_active()
238247
check_string(text)
239-
240248
node <- private$wait_for_selector(css)
249+
p_wait <- private$expand_promise(wait_for)
241250
self$session$DOM$focus(node)
242251
self$session$Input$insertText(text)
243-
252+
self$wait_for(p_wait)
244253
invisible(self)
245254
},
246255

@@ -250,16 +259,29 @@ LiveHTML <- R6::R6Class(
250259
#' keys at <https://pptr.dev/api/puppeteer.keyinput/>.
251260
#' @param modifiers A character vector of modifiers. Must be one or more
252261
#' of `"Shift`, `"Control"`, `"Alt"`, or `"Meta"`.
253-
press = function(css, key_code, modifiers = character()) {
262+
#' @param wait_for What to wait for after the click. Can be `NULL` (the
263+
#' default, waits only for the click event to be sent), `"load"` (waits
264+
#' for a `Page.loadEventFired` event), or a custom promise.
265+
press = function(css, key_code, modifiers = character(), wait_for = NULL) {
254266
private$check_active()
255267
desc <- as_key_desc(key_code, modifiers)
256-
257268
node <- private$wait_for_selector(css)
269+
p_wait <- private$expand_promise(wait_for)
258270
self$session$DOM$focus(node)
259-
260271
exec(self$session$Input$dispatchKeyEvent, type = "keyDown", !!!desc)
261272
exec(self$session$Input$dispatchKeyEvent, type = "keyUp", !!!desc)
273+
self$wait_for(p_wait)
274+
invisible(self)
275+
},
262276

277+
#' @description Wait for a promise to resolve and then sync the session
278+
#' @param promise A promise object to wait for. If `NULL`, the method
279+
#' returns immediately.
280+
wait_for = function(promise = NULL) {
281+
if (promises::is.promise(promise)) {
282+
self$session$wait_for(promise)
283+
private$refresh_root()
284+
}
263285
invisible(self)
264286
}
265287
),
@@ -271,8 +293,6 @@ LiveHTML <- R6::R6Class(
271293
self$session$close()
272294
},
273295

274-
loadEvent_promise = NULL,
275-
276296
check_active = function() {
277297
if (new_chromote && !self$session$is_active()) {
278298
suppressMessages({
@@ -297,46 +317,89 @@ LiveHTML <- R6::R6Class(
297317

298318
find_nodes = function(css, xpath) {
299319
check_exclusive(css, xpath)
320+
300321
if (!missing(css)) {
301-
node_ids <- private$nodes_from_css(css)
302-
unlist(node_ids)
322+
private$find_nodes_css(css)
303323
} else {
304-
search <- glue::glue("
305-
(function() {{
306-
const xpathResult = document.evaluate('{xpath}', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
307-
const nodes = [];
308-
for (let i = 0; i < xpathResult.snapshotLength; i++) {{
309-
nodes.push(xpathResult.snapshotItem(i));
310-
}}
311-
return(nodes);
312-
}})();
313-
")
314-
315-
object_id <- self$session$Runtime$evaluate(search)$result$objectId
316-
props <- self$session$Runtime$getProperties(object_id, ownProperties = TRUE)
317-
318-
ids <- map_chr(props$result, function(prop) prop$value$objectId %||% NA_character_)
319-
# Drop non-nodes
320-
ids <- ids[!is.na(ids)]
321-
322-
unlist(map(ids, self$session$DOM$requestNode), use.names = FALSE)
324+
private$find_nodes_xpath(xpath)
323325
}
324326
},
325327

326-
nodes_from_css = function(css, retry = TRUE) {
328+
find_nodes_css = function(css, retry = TRUE) {
329+
try_fetch(
330+
unlist(self$session$DOM$querySelectorAll(private$root_id, css)$nodeIds),
331+
error = function(cnd) {
332+
if (retry) {
333+
Sys.sleep(0.1)
334+
private$refresh_root()
335+
private$find_nodes_css(css, retry = FALSE)
336+
} else {
337+
cli::cli_abort(
338+
c(
339+
"Failed to find selector.",
340+
"i" = "The page may have changed after your last action.",
341+
"*" = "Try using `wait_for = \"load\"` in the action that caused the navigation."
342+
),
343+
parent = cnd
344+
)
345+
}
346+
}
347+
)
348+
},
349+
350+
find_nodes_xpath = function(xpath, retry = TRUE) {
327351
try_fetch(
328-
self$session$DOM$querySelectorAll(private$root_id, css)$nodeIds,
352+
{
353+
search <- glue::glue("
354+
(function() {{
355+
const xpathResult = document.evaluate('{xpath}', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
356+
const nodes = [];
357+
for (let i = 0; i < xpathResult.snapshotLength; i++) {{
358+
nodes.push(xpathResult.snapshotItem(i));
359+
}}
360+
return(nodes);
361+
}})();
362+
")
363+
364+
object_id <- self$session$Runtime$evaluate(search)$result$objectId
365+
props <- self$session$Runtime$getProperties(object_id, ownProperties = TRUE)
366+
367+
ids <- map_chr(props$result, function(prop) prop$value$objectId %||% NA_character_)
368+
# Drop non-nodes
369+
ids <- ids[!is.na(ids)]
370+
371+
unlist(map(ids, self$session$DOM$requestNode), use.names = FALSE)
372+
},
329373
error = function(cnd) {
330374
if (retry) {
331-
self$session$wait_for(private$loadEvent_promise)
332-
private$nodes_from_css(css, retry = FALSE)
375+
Sys.sleep(0.1)
376+
private$refresh_root()
377+
private$find_nodes_xpath(xpath, retry = FALSE)
333378
} else {
334-
cli::cli_abort(cnd)
379+
cli::cli_abort(
380+
c(
381+
"Failed to find xpath.",
382+
"i" = "The page may have changed after your last action.",
383+
"*" = "Try using `wait_for = \"load\"` in the action that caused the navigation."
384+
),
385+
parent = cnd
386+
)
335387
}
336388
}
337389
)
338390
},
339391

392+
# To support more `wait_for` keywords, add them here.
393+
expand_promise = function(wait_for = NULL) {
394+
if (identical(wait_for, "load")) {
395+
self$session$Page$loadEventFired(wait_ = FALSE)
396+
} else if (promises::is.promise(wait_for)) {
397+
wait_for
398+
} else {
399+
NULL
400+
}
401+
},
402+
340403
# Inspired by https://github.com/rstudio/shinytest2/blob/v1/R/chromote-methods.R
341404
call_node_method = function(node_id, method, ...) {
342405
js_fun <- paste0("function() { return this", method, "}")
@@ -360,12 +423,12 @@ now <- function() proc.time()[[3]]
360423

361424
#' @export
362425
html_table.LiveHTML <- function(x,
363-
header = NA,
364-
trim = TRUE,
365-
fill = deprecated(),
366-
dec = ".",
367-
na.strings = "NA",
368-
convert = TRUE) {
426+
header = NA,
427+
trim = TRUE,
428+
fill = deprecated(),
429+
dec = ".",
430+
na.strings = "NA",
431+
convert = TRUE) {
369432

370433
tables <- html_elements(x, "table")
371434
html_table(

0 commit comments

Comments
 (0)