Instructions for coding agents

This page is for an AI coding agent working in a Rails app that uses, or is moving to, rails-jsdom. It covers setting the gem up, writing interaction tests, and converting an existing system test suite. The full API reference is at jakehow.github.io/rails-jsdom/. This page is also published as Markdown at jakehow.github.io/rails-jsdom/agents.md.

What rails-jsdom is

Set up an app

  1. Add gem "rails-jsdom" to the :test group of the Gemfile and run bundle install.

  2. Run bin/rails generate rails_jsdom:install. It:

  3. requires the library in test/test_helper.rb;

  4. creates test/interactions/;

  5. adds jsdom and esbuild to package.json and runs npm install;

  6. sets min-release-age=7 in .npmrc;

  7. ignores node_modules;

  8. warns if config/cable.yml does not use the test adapter in the test environment.

  9. Make CI install the Node packages before the tests, with npm ci after setting up Node 22 or later.

  10. Commit package.json, package-lock.json and .npmrc.

Write an interaction test

require "test_helper"

class CartInteractionTest < RailsJsdom::TestCase
  setup do
    sign_in_as users(:shopper)
    visit product_path(products(:mug))
  end

  test "adding an item updates the cart" do
    fill_in "Quantity", with: 2
    click_on "Add to cart"

    assert_selector "#cart-count", text: "2"
    assert_text "Added to your cart"
  end
end

Convert a system test suite

Work through test/system/ one file at a time.

  1. Sort each test. Convert it if it checks behaviour: what the page shows, what is hidden, where it navigates, what Rails receives. Keep it as a system test, or delete it, if it needs what jsdom cannot do:

  2. computed CSS, layout or geometry;

  3. CSS media-query breakpoints;

  4. screenshots;

  5. drag and drop;

  6. real iframes or new windows;

  7. custom ActionCable channels;

  8. third-party scripts from a CDN.

  9. Move the file from test/system/ to test/interactions/, and change the test class’s superclass from ApplicationSystemTestCase to RailsJsdom::TestCase.

  10. Remove browser setup: driven_by, using_wait_time, Capybara.* settings, sleep, wait_for_* helpers, take_screenshot and window-size code.

  11. Replace sign-in. System tests often sign in by filling in the login form. That still works. A faster equivalent is the app’s integration-test helper that posts to the session controller.

  12. Translate the methods with the table below. Most methods keep their names. The common changes:

  13. CSS or layout assertions become behaviour assertions (assert_selector ".panel[hidden]", not checks of display or position);

  14. page.driver and browser-specific calls are removed;

  15. send_keys is limited to the keys listed in the table notes.

  16. Run the converted file with bin/rails test test/interactions/<file> and fix any failure before moving on. Typical failures:

  17. an unexpected dialog: wrap the action in the matching accept_* or dismiss_*;

  18. a request to another host: stub it with stub_fetch;

  19. a missing browser API: stub it in the test with execute_script;

  20. an element found more than once: scope it with within or pass match: :first.

  21. When test/system/ is empty, remove ApplicationSystemTestCase, the capybara and selenium-webdriver gems, and the CI steps that installed a browser.

Interaction tests are faster than system tests, so do not merge tests to save time. Keep one behaviour per test. Group checks of the same result into one test only when they would otherwise repeat the same steps.

System test methods in rails-jsdom

βœ… works in rails-jsdom. ❌ does not. Each link goes to the method’s reference.

System test method Status Notes
visit βœ…
refresh βœ… Requests the current URL again, without a history entry
go_back, go_forward βœ… Across Turbo Drive visits and full page loads
using_session βœ… Each session has its own cookies and page; broadcasts reach a session’s page when the test returns to it
click_on, click_link_or_button βœ…
click_link βœ…
click_button βœ…
fill_in βœ… Text-like fields only, as in system tests
check, uncheck βœ…
choose βœ…
select, unselect βœ…
attach_file βœ… Forms submit files to Rails as multipart requests
send_keys βœ… Keys perform the common browser default actions; typing happens at the end of the value, with no caret or selection
within βœ… CSS selector or an element from find; also scopes assert_dom
within_fieldset, within_table βœ…
find, all, first βœ… text:, exact_text:, visible: and the field filters
find_field, find_button, find_link βœ… with:, checked:, unchecked:, disabled:, href:, type:
Element .click, .set, .text, .value, .[], .checked?, .selected?, .disabled?, .visible?, .send_keys, .select_option, .hover βœ… Plus the finders and assertions inside the element; using an element that left the page raises
page βœ… Every helper, plus body and title
assert_text, assert_no_text, refute_text βœ… String or Regexp
assert_selector, assert_css βœ… Counts visible elements, as in system tests; count:, minimum:, maximum:
assert_no_selector, assert_no_css, refute_selector βœ…
assert_field, assert_no_field βœ…
assert_checked_field, assert_unchecked_field βœ…
assert_button, assert_no_button βœ… Disabled buttons match only with disabled:
assert_link, assert_no_link βœ…
assert_current_path, assert_no_current_path βœ… ignore_query:, url:, Regexp
assert_title, assert_no_title βœ…
has_text?, has_content?, has_no_text?, has_no_content? βœ…
has_selector?, has_css?, has_no_selector?, has_no_css? βœ…
has_field?, has_no_field?, has_checked_field?, has_unchecked_field? βœ…
has_button?, has_no_button?, has_link?, has_no_link? βœ…
has_current_path?, has_no_current_path?, has_title?, has_no_title? βœ…
current_path, current_url βœ…
evaluate_script βœ… Awaits a returned promise
execute_script βœ…
accept_alert βœ… An unexpected alert fails the test
accept_confirm, dismiss_confirm βœ… An unexpected confirm fails the test
accept_prompt, dismiss_prompt βœ… An unexpected prompt fails the test
hover, double_click, right_click βœ… Element methods in system tests; here they also take a locator. CSS :hover styles do not apply
within_frame ❌ Not planned: real iframes are mostly cross-origin; Turbo Frames already work
within_window, switch_to_window ❌ Not planned: check a target="_blank" link with assert_link, then visit it
drag_to ❌ Never: drag-and-drop needs layout, which jsdom does not have
take_screenshot ❌ Never: jsdom does not render pages
Waiting (default_max_wait_time, retrying assertions) β€” Not needed: the page settles before every action and assertion

Limits