@@ -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
362425html_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