module RailsJsdom::TestHelper
Helpers for driving a page in jsdom from an integration test. Their names and options follow the system test methods you already use.
Inherit from RailsJsdom::TestCase, which includes this module, or include it in your own ActionDispatch::IntegrationTest.
Locators
Actions find their element with a locator, tried in this order. The first rule that matches anything wins:
-
Stimulus action shorthand.
click "dropdown#toggle"finds the element whosedata-actioncallsdropdown#toggle. -
Text.
-
For
click,click_on,click_link,click_button,hover,double_click,right_click,find_buttonandfind_link: a button, link, submit, checkbox, radio,summary,labelor[role=button]whose text,aria-label,title,idor button value equals the locator, or else contains it. -
For
fill_in,check,uncheck,choose,attach_file,find_fieldand thefrom:ofselect: the field of alabelwhose text equals, or else contains, the locator; then a field whoseplaceholder,name,idoraria-labelequals it. -
CSS selector.
click "#checkout".
click_link only matches links, click_button only buttons, click_on both; fill_in only text-like fields; check and uncheck only checkboxes; choose only radio buttons; attach_file only file fields.
More than one match raises, unless you pass match: :first.
Filters
find, all, first, find_field, find_button, find_link, the has_*? predicates and the assert_* element assertions take these filters:
-
text:the visible text includes a String, or matches a Regexp. -
exact_text:the visible text equals a String, or matches a Regexp. -
visible:true(the default) only visible elements;falseor:allevery element;:hiddenonly hidden ones. -
with:a field’s current value. -
checked:,unchecked:a checkbox or radio button’s state. -
disabled:false(the default for fields and buttons),true, or:all. -
href:a link’shref. -
type:an input’s type, or the tag name of other fields.
Count assertions also take count:, or minimum: and maximum:.
Scoping
within, within_fieldset and within_table scope every action, finder and assertion in their block, including assert_dom, to one element. Blocks nest.
Dialogs
An alert, confirm or prompt that appears during an action fails it, unless the action runs inside the matching accept_alert, accept_confirm,
dismiss_confirm, accept_prompt or dismiss_prompt.
Settling
Every action returns once the page has settled: due timers have run on a virtual clock, no Rails request is pending, and Turbo Stream broadcasts made so far have been applied. JavaScript errors and unhandled promise rejections raised during the action fail it. Assertions and predicates read the settled page, so they never need to wait or retry.
Public Instance Methods
# File lib/rails_jsdom/test_helper.rb, line 353 def accept_alert(text = nil, &block) rails_jsdom_modal("alert", true, text, &block) end
Accepts every alert that appears while the block runs, and returns the first alert message that matches text (a String the message must include, or a Regexp). Raises RailsJsdom::Error if no matching alert appeared.
accept_alert “Saved” do click_on “Save” end
# File lib/rails_jsdom/test_helper.rb, line 363 def accept_confirm(text = nil, &block) rails_jsdom_modal("confirm", true, text, &block) end
Accepts every confirm dialog that appears while the block runs. It matches and returns the message like accept_alert.
accept_confirm “Remove the first item?” do click_on “Remove” end
# File lib/rails_jsdom/test_helper.rb, line 382 def accept_prompt(text = nil, with: nil, &block) rails_jsdom_modal("prompt", true, text, with: with, &block) end
Answers every prompt that appears while the block runs with with:, or with the prompt’s default value when with: is left out. It matches and returns the message like accept_alert.
accept_prompt with: “Ada” do click_on “Rename” end
Source
# File lib/rails_jsdom/test_helper.rb, line 321 def all(selector, **filters) rails_jsdom_elements(Query.new("css", selector, **filters)) end
Every element that matches the CSS selector and the filters, possibly none.
all(“.cart-item”).map(&:text)
# File lib/rails_jsdom/test_helper.rb, line 566 def assert_checked_field(locator = nil, **filters) assert_field(locator, checked: true, **filters) end
Asserts that the checkbox or radio button found by locator is checked.
# File lib/rails_jsdom/test_helper.rb, line 513 def assert_current_path(path, ignore_query: false, url: false) actual = rails_jsdom_current(ignore_query, url) assert Query.same?(actual, path), "Expected the current path to be #{path.inspect}, but it was #{actual.inspect}" end
Asserts that the current path and query equal path, or match it when it is a Regexp. Pass ignore_query: true to compare the path only, or url: true to compare the whole URL.
assert_current_path project_path(project)
# File lib/rails_jsdom/test_helper.rb, line 556 def assert_field(locator = nil, count: nil, minimum: nil, maximum: nil, **filters) rails_jsdom_assert_count(Query.new("field", locator, **filters), count, minimum, maximum) end
Asserts that a field found by locator matches the filters.
assert_field “Quantity”, with: “2”
# File lib/rails_jsdom/test_helper.rb, line 590 def assert_link(locator = nil, count: nil, minimum: nil, maximum: nil, **filters) rails_jsdom_assert_count(Query.new("link", locator, **filters), count, minimum, maximum) end
Asserts that a link found by locator matches the filters.
assert_link “Second page”, href: “/second”
# File lib/rails_jsdom/test_helper.rb, line 519 def assert_no_current_path(path, ignore_query: false, url: false) actual = rails_jsdom_current(ignore_query, url) assert_not Query.same?(actual, path), "Expected the current path not to be #{path.inspect}" end
Asserts that the current path does not match path.
# File lib/rails_jsdom/test_helper.rb, line 561 def assert_no_field(locator = nil, **filters) rails_jsdom_assert_none(Query.new("field", locator, **filters)) end
Asserts that no field found by locator matches the filters.
# File lib/rails_jsdom/test_helper.rb, line 595 def assert_no_link(locator = nil, **filters) rails_jsdom_assert_none(Query.new("link", locator, **filters)) end
Asserts that no link found by locator matches the filters.
# File lib/rails_jsdom/test_helper.rb, line 547 def assert_no_selector(selector, **filters) rails_jsdom_assert_none(Query.new("css", selector, **filters)) end
Asserts that no element matches the CSS selector and the filters.
Source
# File lib/rails_jsdom/test_helper.rb, line 503 def assert_no_text(text) assert has_no_text?(text), "Expected the page text not to include #{text.inspect}" end
Asserts that the visible text does not include text.
Source
# File lib/rails_jsdom/test_helper.rb, line 531 def assert_no_title(title) assert has_no_title?(title), "Expected the title not to include #{title.inspect}" end
Asserts that the page title does not include title.
# File lib/rails_jsdom/test_helper.rb, line 541 def assert_selector(selector, count: nil, minimum: nil, maximum: nil, **filters) rails_jsdom_assert_count(Query.new("css", selector, **filters), count, minimum, maximum) end
Asserts that elements match the CSS selector and the filters: at least one by default, exactly count:, or between minimum: and maximum:. Only visible elements count, unless you pass visible: false.
assert_selector “.cart-item”, count: 2 assert_selector “li”, text: “Mug”
Source
# File lib/rails_jsdom/test_helper.rb, line 498 def assert_text(text) assert has_text?(text), "Expected the page text to include #{text.inspect}" end
Asserts that the visible text includes text, or matches it when it is a Regexp.
Source
# File lib/rails_jsdom/test_helper.rb, line 526 def assert_title(title) assert has_title?(title), "Expected the title to include #{title.inspect}, but it was #{rails_jsdom_snapshot.fetch("title").inspect}" end
Asserts that the page title includes title, or matches it when it is a Regexp.
# File lib/rails_jsdom/test_helper.rb, line 572 def assert_unchecked_field(locator = nil, **filters) assert_field(locator, unchecked: true, **filters) end
Asserts that the checkbox or radio button found by locator is unchecked.
# File lib/rails_jsdom/test_helper.rb, line 242 def attach_file(locator, paths, match: nil) files = Array(paths).map { |path| { name: File.basename(path), type: Rack::Mime.mime_type(File.extname(path.to_s)), base64: [ File.binread(path) ].pack("m0") } } rails_jsdom_act("attach", locator, match, files: files) end
Puts the files at paths (one path or an array) in the file field found by locator, replacing any files it had, and dispatches input and change. Several files need a field with multiple. A form submits the files to Rails as a multipart request, so controllers receive ActionDispatch::Http::UploadedFile params.
attach_file “Avatar”, file_fixture(“avatar.png”)
Source
# File lib/rails_jsdom/test_helper.rb, line 203 def check(locator, match: nil) rails_jsdom_act("check", locator, match, checked: true) end
Checks the checkbox found by locator, unless it is already checked.
check “Remember me”
Source
# File lib/rails_jsdom/test_helper.rb, line 215 def choose(locator, match: nil) rails_jsdom_act("choose", locator, match) end
Selects the radio button found by locator.
choose “Monthly”
Source
# File lib/rails_jsdom/test_helper.rb, line 146 def click(locator, match: nil) rails_jsdom_act("click", locator, match) end
Clicks any element found by locator (see Locators) and settles the page. Turbo links and forms are answered by Rails in-process.
click “dropdown#toggle”
# File lib/rails_jsdom/test_helper.rb, line 161 def click_link(locator, match: nil) rails_jsdom_act("click", locator, match, kind: "link") end
Clicks the link found by locator.
click_link “Second page”
# File lib/rails_jsdom/test_helper.rb, line 153 def click_on(locator, match: nil) rails_jsdom_act("click", locator, match, kind: "link_or_button") end
Clicks the link or button found by locator.
click_on “Add to cart”
Source
# File lib/rails_jsdom/test_helper.rb, line 393 def current_path URI(current_url).path end
The path of the page’s current URL, after any Turbo navigation.
Source
# File lib/rails_jsdom/test_helper.rb, line 398 def current_url rails_jsdom_snapshot.fetch("url") end
The page’s current URL, after any Turbo navigation.
# File lib/rails_jsdom/test_helper.rb, line 371 def dismiss_confirm(text = nil, &block) rails_jsdom_modal("confirm", false, text, &block) end
Dismisses every confirm dialog that appears while the block runs. It matches and returns the message like accept_alert.
dismiss_confirm { click_on “Remove” }
# File lib/rails_jsdom/test_helper.rb, line 388 def dismiss_prompt(text = nil, &block) rails_jsdom_modal("prompt", false, text, &block) end
Cancels every prompt that appears while the block runs. It matches and returns the message like accept_alert.
Source
# File lib/rails_jsdom/test_helper.rb, line 602 def document_root_element html = rails_jsdom_snapshot.fetch("html") rails_jsdom_scope.empty? ? Nokogiri::HTML5(html).root : Nokogiri::HTML5.fragment(html, context: "template").element_children.first end
The root of the live page’s DOM, or the current within element. Rails uses it for assert_dom, assert_select and css_select, so those assertions read the page.
# File lib/rails_jsdom/test_helper.rb, line 182 def double_click(locator, match: nil) rails_jsdom_act("double_click", locator, match) end
Clicks the element found by locator twice, then dispatches dblclick.
Source
# File lib/rails_jsdom/test_helper.rb, line 335 def evaluate_script(script) rails_jsdom_perform(command: "evaluate", script: script) end
Runs script in the page, settles it and returns the script’s value, awaited if it is a promise. The value must be JSON-serialisable.
evaluate_script(“document.title”) # => “Projects”
Source
# File lib/rails_jsdom/test_helper.rb, line 340 def execute_script(script) evaluate_script(script) nil end
Runs script in the page and settles it, returning nothing.
# File lib/rails_jsdom/test_helper.rb, line 196 def fill_in(locator, with:, match: nil) rails_jsdom_act("fill_in", locator, match, with: with.to_s) end
Sets the value of the text-like field found by locator, dispatches input and change, and settles the page.
fill_in “Quantity”, with: 2
# File lib/rails_jsdom/test_helper.rb, line 295 def find(selector, match: nil, **filters) rails_jsdom_find(Query.new("css", selector, **filters), match) end
The one element that matches the CSS selector and the filters. Raises RailsJsdom::Error when none matches, or when several do unless you pass match: :first.
find(“.cart-item”, text: “Mug”).click
# File lib/rails_jsdom/test_helper.rb, line 303 def find_field(locator = nil, match: nil, **filters) rails_jsdom_find(Query.new("field", locator, **filters), match) end
The one field found by locator (see Locators) that matches the filters, or the only field when locator is left out.
find_field(“Quantity”, with: “2”)
# File lib/rails_jsdom/test_helper.rb, line 313 def find_link(locator = nil, match: nil, **filters) rails_jsdom_find(Query.new("link", locator, **filters), match) end
The one link found by locator that matches the filters.
Source
# File lib/rails_jsdom/test_helper.rb, line 327 def first(selector, **filters) rails_jsdom_find(Query.new("css", selector, **filters), :first) end
The first element that matches the CSS selector and the filters. Raises RailsJsdom::Error when none matches.
Source
# File lib/rails_jsdom/test_helper.rb, line 88 def go_back rails_jsdom_perform(command: "traverse", delta: -1) end
Goes back one history entry: a Turbo Drive visit or a full page load. Does nothing on the first page.
Source
# File lib/rails_jsdom/test_helper.rb, line 93 def go_forward rails_jsdom_perform(command: "traverse", delta: 1) end
Goes forward one history entry after go_back.
# File lib/rails_jsdom/test_helper.rb, line 467 def has_checked_field?(locator = nil, **filters) has_field?(locator, checked: true, **filters) end
Whether a checked checkbox or radio button is found by locator.
# File lib/rails_jsdom/test_helper.rb, line 423 def has_current_path?(path, ignore_query: false, url: false) Query.same?(rails_jsdom_current(ignore_query, url), path) end
Whether the current path matches path, as assert_current_path compares it.
# File lib/rails_jsdom/test_helper.rb, line 457 def has_field?(locator = nil, count: nil, minimum: nil, maximum: nil, **filters) rails_jsdom_counted?(Query.new("field", locator, **filters), count, minimum, maximum) end
Whether a field found by locator matches the filters.
# File lib/rails_jsdom/test_helper.rb, line 487 def has_link?(locator = nil, count: nil, minimum: nil, maximum: nil, **filters) rails_jsdom_counted?(Query.new("link", locator, **filters), count, minimum, maximum) end
Whether a link found by locator matches the filters.
# File lib/rails_jsdom/test_helper.rb, line 428 def has_no_current_path?(path, **options) !has_current_path?(path, **options) end
Whether the current path does not match path.
# File lib/rails_jsdom/test_helper.rb, line 462 def has_no_field?(locator = nil, **filters) rails_jsdom_matches(Query.new("field", locator, **filters)).empty? end
Whether no field found by locator matches the filters.
# File lib/rails_jsdom/test_helper.rb, line 492 def has_no_link?(locator = nil, **filters) rails_jsdom_matches(Query.new("link", locator, **filters)).empty? end
Whether no link found by locator matches the filters.
# File lib/rails_jsdom/test_helper.rb, line 451 def has_no_selector?(selector, **filters) rails_jsdom_matches(Query.new("css", selector, **filters)).empty? end
Whether no element matches the CSS selector and the filters.
Source
# File lib/rails_jsdom/test_helper.rb, line 416 def has_no_text?(text) !has_text?(text) end
Whether the visible text does not include text.
Source
# File lib/rails_jsdom/test_helper.rb, line 439 def has_no_title?(title) !has_title?(title) end
Whether the page title does not include title.
# File lib/rails_jsdom/test_helper.rb, line 445 def has_selector?(selector, count: nil, minimum: nil, maximum: nil, **filters) rails_jsdom_counted?(Query.new("css", selector, **filters), count, minimum, maximum) end
Whether elements match the CSS selector and the filters: at least one, exactly count:, or between minimum: and maximum:.
Source
# File lib/rails_jsdom/test_helper.rb, line 410 def has_text?(text) Query.like?(page_text, text) end
Whether the visible text includes text, or matches it when it is a Regexp.
Source
# File lib/rails_jsdom/test_helper.rb, line 434 def has_title?(title) Query.like?(rails_jsdom_snapshot.fetch("title"), title) end
Whether the page title includes title, or matches it when it is a Regexp.
# File lib/rails_jsdom/test_helper.rb, line 472 def has_unchecked_field?(locator = nil, **filters) has_field?(locator, unchecked: true, **filters) end
Whether an unchecked checkbox or radio button is found by locator.
Source
# File lib/rails_jsdom/test_helper.rb, line 177 def hover(locator, match: nil) rails_jsdom_act("hover", locator, match) end
Moves the pointer over the element found by locator, dispatching pointerover, pointerenter, mouseover, mouseenter, pointermove and mousemove. CSS :hover styles are not applied.
hover “Help”
Source
# File lib/rails_jsdom/test_helper.rb, line 123 def mount_html(html) rails_jsdom_perform(command: "load", html: RailsJsdom.document(html), url: "http://#{host}/", history: "push") end
Loads an HTML fragment as the body of a page on the application host.
mount_html %(<div data-controller=“counter”></div>)
# File lib/rails_jsdom/test_helper.rb, line 116 def mount_partial(partial, locals: {}, assigns: {}) mount_html(ApplicationController.render(partial: partial, locals: locals, assigns: assigns)) end
Renders partial with ApplicationController.render and loads it as a page, so its Stimulus controllers connect. No route is needed.
mount_partial “shared/counter”, locals: { start: 2 }
Source
# File lib/rails_jsdom/test_helper.rb, line 138 def page @rails_jsdom_page ||= Page.new(self) end
The page: an object with every helper in this module, plus RailsJsdom::Page#body and RailsJsdom::Page#title.
Source
# File lib/rails_jsdom/test_helper.rb, line 404 def page_text rails_jsdom_snapshot.fetch("text") end
The visible text of the page, or of the current within scope. Hidden elements, templates, scripts and closed dialogs are left out.
Source
# File lib/rails_jsdom/test_helper.rb, line 82 def refresh rails_jsdom_visit(URI(current_url).request_uri, "none") end
Requests the current URL again and loads it as a new page, without adding a history entry.
# File lib/rails_jsdom/test_helper.rb, line 188 def right_click(locator, match: nil) rails_jsdom_act("right_click", locator, match) end
Dispatches a right-button mousedown, mouseup and contextmenu on the element found by locator.
# File lib/rails_jsdom/test_helper.rb, line 224 def select(value, from: nil, match: nil) rails_jsdom_perform(command: "select", option: value.to_s, from: from, selected: true, scope: rails_jsdom_scope, match: match) end
Selects the option whose text equals, or else contains, value, in the select found by the from: locator, or in any select when from: is left out. Dispatches input and change.
select “Canada”, from: “Country”
Source
# File lib/rails_jsdom/test_helper.rb, line 264 def send_keys(*keys) rails_jsdom_perform(command: "send_keys", keys: RailsJsdom.keys(keys)) end
Types keys into the focused element. A String types its characters; a Symbol presses a key: :enter, :tab, :backspace, :delete, :escape, :space, :left, :right, :up, :down, :home, :end, :page_up or :page_down. An Array presses its last key while holding the modifiers before it: :shift, :control, :alt, :meta.
Each key dispatches keydown, keypress for characters and keyup. Unless a listener prevents it, the key then does what a browser does: characters and Backspace edit the end of a text field, Enter submits the field’s form or clicks a focused button or link, Space clicks a focused button or checkbox, Tab and Shift+Tab move the focus (firing change on a field you typed in), and the arrow keys, Home and End step a range or number field. Typing always happens at the end of the value: there is no caret or selection.
find_field(“Search”).send_keys(“mugs”, :enter) send_keys [ :shift, :tab ]
# File lib/rails_jsdom/test_helper.rb, line 132 def stub_fetch(url, body: "", status: 200, headers: { "content-type" => "application/json" }) RailsJsdom.bridge.call(command: "stub_fetch", url: url, response: { status: status, headers: headers, body: body }) end
Answers the page’s requests to url with the given response instead of sending them to Rails. Call it after the page is loaded; visit and the mount_* methods clear all stubs.
stub_fetch “/api/agents”, body: [].to_json
# File lib/rails_jsdom/test_helper.rb, line 208 def uncheck(locator, match: nil) rails_jsdom_act("check", locator, match, checked: false) end
Unchecks the checkbox found by locator, unless it is already unchecked.
# File lib/rails_jsdom/test_helper.rb, line 231 def unselect(value, from: nil, match: nil) rails_jsdom_perform(command: "select", option: value.to_s, from: from, selected: false, scope: rails_jsdom_scope, match: match) end
Deselects the option value in a select that allows multiple options.
unselect “Red”, from: “Colours”
# File lib/rails_jsdom/test_helper.rb, line 104 def using_session(name) previous = rails_jsdom_session_name rails_jsdom_switch_session(name.to_s) yield ensure rails_jsdom_switch_session(previous) end
Runs the block as another user, with its own cookies and its own page. The other session’s page stays as it was, and receives Turbo Stream broadcasts when the test returns to it.
using_session(:ada) { sign_in_as users(:ada); visit chat_path } using_session(:grace) { visit chat_path; fill_in “Message”, with: “Hi”; click_on “Send” } using_session(:ada) { assert_text “Hi” }
Source
# File lib/rails_jsdom/test_helper.rb, line 76 def visit(path) rails_jsdom_visit(path, "push") end
Requests path with GET through Rails, follows redirects, loads the response into a new page and settles it.
visit project_path(projects(:billing))
Source
# File lib/rails_jsdom/test_helper.rb, line 275 def within(selector, &block) rails_jsdom_within(selector.is_a?(Element) ? { handle: selector.handle } : { css: selector }, &block) end
Scopes every action, finder and assertion in the block to the one element that matches the CSS selector, or to an element from find.
within “#cart” do click_on “Remove” assert_no_text “Mug” end
# File lib/rails_jsdom/test_helper.rb, line 281 def within_fieldset(locator, &block) rails_jsdom_within({ fieldset: locator }, &block) end
Like within, for the fieldset whose id or legend text is locator.
# File lib/rails_jsdom/test_helper.rb, line 286 def within_table(locator, &block) rails_jsdom_within({ table: locator }, &block) end
Like within, for the table whose id or caption text is locator.