Dommy::Rack
dommy-rack lets a Rack application (including Rails) be visited and manipulated as a Dommy Document, without launching a real browser.
It provides a small, synchronous, browser-like session API: navigation, cookies, redirects, link clicking, form submission, JSON requests, and simple matchers.
There is no JavaScript engine and no network: requests are dispatched straight to your Rack app object, and responses are parsed into a Dommy DOM. This makes it a fast backend for integration tests and a building block for higher-level drivers such as capybara-dommy.
Installation
# Gemfile
gem "dommy-rack"
bundle install
Quick start
require "dommy/rack"
session = Dommy::Rack::Session.new(MyRackApp)
session.visit("/")
session.click_link("New post")
session.fill_in("post[title]", with: "Hello")
session.("Create")
session.current_path # => "/posts/1"
session.at_css(".notice").text_content # => "Created"
Session.new accepts any Rack-callable app plus options:
| option | default | meaning |
|---|---|---|
default_host |
"http://example.org" |
base for relative URLs |
follow_redirects |
true |
follow 3xx responses |
max_redirects |
5 |
redirect / meta-refresh limit |
respect_method_override |
true |
honor Rails _method |
method_override_param |
"_method" |
override param name |
user_agent |
"DommyRack" |
default User-Agent |
accept |
HTML accept string | default Accept |
enforce_same_origin |
true |
block cross-origin requests |
follow_meta_refresh |
true |
follow <meta http-equiv="refresh"> (delay 0) |
Navigation
session.visit("/path")
session.get("/path", headers: {})
session.post("/path", params: {a: 1})
session.put("/path", params: {})
session.patch("/path", params: {})
session.delete("/path")
session.request("REPORT", "/path", params: {})
session.reload
session.back
session.forward
session.current_url # full URL
session.current_path # path component
session.current_host
fetch issues a request without changing the current page or history, and returns the Response directly:
res = session.fetch("/api/ping", redirect: :manual) # :follow | :manual | :error
res.status
Inspecting the current page
session.status # last response status (Integer)
session.headers # last response headers (Hash)
session.body # raw response body (String)
session.html # serialized document HTML
session.text # document body text
session.success? # 2xx
session.not_found? # 404
session.client_error? # 4xx
session.server_error? # 5xx
session.save_page # write HTML to a temp file, returns path
session.save_page("out.html") # write to a specific path
DOM queries
session.at_css("h1") # first match (a Dommy element) or nil
session.all_css("li") # all matches
session.at_xpath("//h1")
session.all_xpath("//li")
Redirect chain
session.visit("/a") # /a -> /b -> /c
session.redirected? # => true
session.redirects # => [{status: 302, url: ".../a", location: "/b"}, ...]
Forms
session.fill_in("Email", with: "a@example.com") # by label, id, name, placeholder, aria-label
session.choose("Male") # radio
session.check("Subscribe") # checkbox
session.uncheck("Subscribe")
session.select("Tokyo", from: "City") # <select>
session.unselect("Tokyo", from: "City")
session.attach_file("Avatar", "/path/to/file.png")
session.("Save") # submits the owning form
session.submit_form(session.at_css("form"))
Locators are Capybara-style and depend on the element type:
fields match by id, name, label text, placeholder, or aria-label;
links match by visible text, id, title, or exact href;
buttons match by button text, value, id, name, or alt.
JSON
session.post_json("/api/posts", {title: "Hi"}) # also put_json / patch_json / delete_json
session.status # => 201
session.json # => {"id" => 7}
session.json(symbolize_names: true) # => {id: 7}
# On a Response:
res = session.fetch("/api/posts")
res.json? # content-type is JSON-ish (application/json, text/json, *+json)
res.json # parsed body (parses regardless of content-type)
A String passed to post_json is sent verbatim (already-encoded JSON).
Content-Type and Accept default to application/json and can be overridden via headers:.
Persistent headers and authentication
session.set_header("X-Api-Key", "secret") # sent on every request
session.delete_header("X-Api-Key")
session.default_headers # current persistent headers (copy)
session.basic_auth("alice", "s3cret") # Authorization: Basic ...
session.("token123") # Authorization: Bearer token123
Per-request headers: override persistent defaults (case-insensitively).
Cookies
session. # all cookies
session.("sid")
session.("sid", "42", path: "/")
session.
Cookies set by the app via Set-Cookie are stored and replayed automatically.
Scoping and matchers
session.within("#sidebar") do |s|
s.click_link("Help")
s.has_text?("Contact us") # scoped to #sidebar
end
session.has_css?(".item", count: 3)
session.has_no_css?(".error")
session.has_text?("Welcome")
session.has_no_text?("Error")
session.has_link?("Home")
session.("Save")
session.has_field?("Email")
iframes
session.within_frame("preview") do |s| # by id, name, CSS, or the sole frame
s.has_text?("inside the iframe")
end
within_frame fetches the iframe's src as a sub-document and scopes finds and matchers to it for the block.
Instrumentation
session.on_request { |env| Rails.logger.info("-> #{env["PATH_INFO"]}") }
session.on_response { |response| Rails.logger.info("<- #{response.status}") }
Errors
All errors inherit from Dommy::Rack::Error:
ElementNotFoundError, AmbiguousElementError, ElementNotClickableError,
UnsupportedURLError, CrossOriginError, TooManyRedirectsError,
UnsupportedContentTypeError, InvalidFormError, FileNotFoundError.
Development
bin/setup # install dependencies
rake test # run the test suite
bin/console # interactive prompt
Contributing
Bug reports and pull requests are welcome at https://github.com/takahashim/dommy-rack.
License
Available as open source under the MIT License.