Runtime protocol

rails-jsdom has two halves. The Ruby side (RailsJsdom::Bridge) runs in the test process. The Node side (lib/rails_jsdom/runtime.js) runs the pages in jsdom. This page describes the messages between them, for anyone working on either half. Tests never use it directly.

Transport

Replies from Node

Message Meaning
{ "type": "result", "value": … } The command succeeded. value is JSON; a value JSON cannot carry, such as undefined or a function, becomes null.
{ "type": "error", "message": "…" } The command failed. message includes the JavaScript stack. Ruby raises RailsJsdom::Error with it.
{ "type": "fetch", "id": 7, "request": { … } } Not a reply. The page asked for a URL, and Node waits for Ruby’s answer before the command can finish.

Requests to Rails

While a command runs, the page may request URLs: its own fetch calls, link clicks, form submissions and Turbo visits. Node sends each as a fetch message:

Field Value
request.method "GET", "POST" and so on
request.url The absolute URL
request.headers Header names in lower case
request.body The body as base64, or null. Form bodies are application/x-www-form-urlencoded; forms with files are multipart/form-data, with the boundary in content-type.

Ruby answers with { "type": "fetch_response", "id": 7, "response": { … } }. The response is either:

Requests from a page that has already been replaced are never sent.

Commands

Every command is an object with a command field. Fields marked scope, locator and match are described under Finding elements.

Reads

Reads answer at once and do not change the page.

Command Fields Result
setup root, controllers, modules, stubs, directories, horizon, hidden {} once the Stimulus bundle is built. Sent once, first. See Setup.
snapshot scope { html, text, url, title }: the HTML and visible text of the page or scope, and the page’s URL and title
query strategy ("css", "field", "button" or "link"), locator (or null for every element of that kind), scope An array of element descriptions
read handle One element description
streams none The signed stream names of the page’s <turbo-cable-stream-source> elements for Turbo’s channel
stub_fetch url (full URL, or path and query), response null. Later page requests to that URL get response without asking Rails, until the next load.
expect_modal type ("alert", "confirm" or "prompt"), accept, with (prompt answer or null) null. Dialogs of that type are answered until modal_messages.
modal_messages none The messages of the dialogs shown since expect_modal. Stops expecting dialogs.
session name null. Switches to that session’s page and history; a new name starts with no page.
reset none null. Closes every page and forgets every session, stub and expected dialog. Sent after each test.

Actions

Actions change the page. After an action, Node settles the page before it replies:

An action fails if the page threw an error, rejected a promise, showed an unexpected dialog or never became idle.

Command Fields Does
load html, url, history ("push" or "none") Loads a new page and boots Turbo and Stimulus. "push" adds a history entry. Clears fetch stubs. Result: the page’s id.
click locator, scope, match, kind ("link", "button", "link_or_button" or null) Clicks the element
hover, double_click, right_click locator, scope, match Dispatches the pointer events
fill_in locator, scope, match, with Sets a text-like field and dispatches input and change
check locator, scope, match, checked Clicks a checkbox if its state differs
choose locator, scope, match Clicks a radio button if it is not selected
select option, from (a locator or null for any select), selected, scope, match Selects or deselects the option whose text equals, or else contains, option
attach locator, scope, match, files ([{ name, type, base64 }]) Puts the files in a file field and dispatches input and change
send_keys keys Types into the focused element. See Keys.
element handle, action, value (for set), keys (for send_keys) Runs click, hover, double_click, right_click, set, select_option, unselect_option or send_keys on that element
traverse delta (-1 or 1) Goes back or forward through Turbo history entries, then through full page loads
evaluate script Runs the script in the page. Result: its value, awaited if it is a promise.
deliver messages Renders each Turbo Stream message with Turbo.renderStreamMessage

Setup

Field Value
root The app’s root. Node loads jsdom and esbuild from the app’s node_modules.
controllers The directory of Stimulus controllers. Each *_controller.js is bundled under its identifier (admin/user_panel_controller.js is admin--user-panel).
modules The importmap pins that resolve to local files, as { name: path }
stubs The pin names that resolve to other hosts. They are bundled as empty modules.
directories The importmap pin_all_from directories, as { prefix: path }
horizon How far ahead, in milliseconds, settling runs timers
hidden The CSS selector for elements whose text is not visible

Finding elements

Element descriptions

Field Value
handle The element’s number on this page. The same node keeps its handle. Using a handle whose node has left the page fails.
tag The tag name
text The visible text
all_text All text, including hidden text
visible Whether neither it nor an ancestor is hidden
value The current value, or null for an element without one
checked, selected, disabled The element’s state
type An input’s type, or the tag name of other elements
attributes Its attributes, as { name: value }

Keys

keys is an array. Each entry is one of:

Either kind can add "modifiers", an array of shift, control, alt and meta, held while the key is pressed.