Capybara: Execute asynchronous JavaScript

Updated . Posted . Visible to the public. Repeats.

Capybara provides execute_script and evaluate_script to execute JavaScript code in a Selenium-controlled browser. This however is not a good solution for asynchronous JavaScript.

Enter evaluate_async_script, which allows you to execute some asynchronous code and wait until it finishes. There is a timeout of a couple of seconds, so it will not wait forever.

Use it like this:

page.evaluate_async_script(<<~JS)
  let [done] = arguments
  doSomethingAsynchronous().then(() => {
    done() // call this to indicate we're done
  })
JS

You can return results to Ruby by passing them to the done callback:

result = page.evaluate_async_script(<<~JS)
  let [done] = arguments
  doSomethingAsynchronous().then(() => {
    done("some result")
  })
JS

Finally, you can pass additional object from Ruby to Javascript:

result = page.evaluate_async_script(<<~JS, arg1, arg2)
  let [arg1, arg2, done] = arguments
  doSomethingAsynchronous().then(() => {
    done("some result")
  })
JS

Handling Errors

If Errors happen within your JavaScript code, execution stops but the done callback helper doesn't get called. Selenium unfortunately also doesn't recognize that an error happens and fails with a Timeout Exception after a while. To fix this, append a .catch block that calls the callback method with some information about the Error. The inclusion of error: true in the return value tells Selenium that an error happened:

result = page.evaluate_async_script(<<~JS, arg1, arg2)
  let [arg1, arg2, done] = arguments
  doSomethingAsynchronous().then(() => {
    done("some result")
  }).catch((error) => {
    error => done({error: true, message: error.message, stacktrace: error.stack})
  })
JS

Returning a promise

Warning

The following is not documented anywhere, it may be incorrect.

In newer Capybara versions you may also wrap a promise inside an IIFE and use evaluate_script:

result = page.evaluate_script(<<~JS)
  (function() {
    return doSomethingAsynchronous()
  })()
JS

Of course, you may also add an async-modifier to this function declaration to enable the usage of await. Please check, if this works for you. If not, use evaluate_async_script and chain your calls.

Tobias Kraze
Last edit
Daniel Schulz
License
Source code in this card is licensed under the MIT License.
Posted by Tobias Kraze to makandra dev (2019-11-05 11:09)