Skip to content

Commit

Permalink
Support Capybara assertions in ActionDispatch::IntegrationTest
Browse files Browse the repository at this point in the history
This is a re-submission of #41291.

Both `ActionDispatch::IntegrationTest` and Capybara in `:rack_test` mode
are capable of exercising Rails applications that serve HTML over HTTP.

Capybara itself provides a [wide-range of selectors][selectors], which
can be [extended even further][capybara_accessible_selectors]. While
Capybara's JavaScript far exceeds `ActionDispatch::IntegrationTest`'
HTML-only support, their overlapping capabilities makes Capybara an
interesting candidate to supplant `ActionDispatch::IntegrationTest`'s
assertions.

For example, Capybara provides a `:button` selector, which can be
invoked by `assert_selector :button, "Button Text"` or `assert_button
"Button Text"`. Additionally, Capybara's `button` selector supports
resolution based on ARIA attributes like `aria-label` and
`[role="button"]`. The assertions provided by `rails-dom-testing` do
not.

It's possible to recreate those assertion characteristics in
`rails-dom-testing`, Rails itself, or a consumer application. However,
given the fact that `capybara` is already a Rails testing dependency
(through System Test support), and the fact that they're both capable of
coordinating with `Rack::Test`, there is an opportunity to unify them.

[selectors]: https://github.com/teamcapybara/capybara/tree/84acc29d5ff807507fe57aafcf7f9b2acdb89fe2/lib/capybara/selector/definition
[capybara_accessible_selectors]: https://github.com/citizensadvice/capybara_accessible_selectors/tree/d61971c609e3b019df6dc0ea0c9ce11433f3d0f7#documentation

Action Dispatch: Use `build_rack_mock_session` in test
---

This commit introduces the
`ActionDispatch::Assertions::CapybaraAssertions` module to override the
`ActionDispatch::Integration::Session#_mock_session` implementation by
substituting the `Rack::MockSession.new` call with a memoized delgation
to the an already constructed `Rack::MockSession` instance created by
the `RackTest`-driven `Capybara::Session`.

This commit includes a new test case that extends the `Session` instance
to integrate with `Capybara`.``

Alternatives
---

This is currently possible, without any implementation changes.
Applications could create their own versions of
`ActionDispatch::Assertions::CapybaraAssertions`by overriding
`_mock_session`. That feels like a blatant violation of the public API.
Having said that, if a third-party package were to provide this kind of
Capybara integration, it would have to re-open the class and target the
private `_mock_session` method for overriding.

Similarly, [open_session][]'s documentation mentions the pattern of
using `#extend` on the block's `Session` instance. However, I'm not sure
if there's a way to promote the block argument to serve globally as the
overridded `#integration_session` that methods are delegated to.

[Rack::Test::Methods]: https://github.com/rack/rack-test/blob/v1.1.0/lib/rack/test/methods.rb#L29-L31
[Capybara::RackTest::Browser]: https://github.com/teamcapybara/capybara/blob/3.35.3/lib/capybara/rack_test/browser.rb#L128-L131
[open_session]: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/integration.rb#L377-L386
  • Loading branch information
seanpdoyle committed Sep 10, 2022
1 parent e4990ee commit 10d5797
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 0 deletions.
5 changes: 5 additions & 0 deletions actionpack/CHANGELOG.md
@@ -1,3 +1,8 @@
* Introduce `ActionDispatch::Assertions::CapybaraAssertions` to support
`Capybara` assertions in `ActionDispatch::IntegrationTest`

*Sean Doyle*

* Rescue `JSON::ParserError` in Cookies json deserializer to discards marshal dumps:

Without this change, if `action_dispatch.cookies_serializer` is set to `:json` and
Expand Down
81 changes: 81 additions & 0 deletions actionpack/lib/action_dispatch/testing/assertions/capybara.rb
@@ -0,0 +1,81 @@
# frozen_string_literal: true

gem "capybara"
require "capybara"

module ActionDispatch
module Assertions
# Substitute <tt>rails-dom-testing</tt>-provided assertions with <tt>Capybara</tt>-powered assertions.
#
# By default, assertions about the contents of the response's body are provided by [rails-dom-testing][].
#
# Action Dispatch can also integrate with Capybara's selectors and assertions by including the <tt>ActionDispatch::Assertions::CapybaraAssertions</tt> module:
#
#
# require "test_helper"
# require "action_dispatch/testing/assertions/capybara"
#
# class BlogFlowTest < ActionDispatch::IntegrationTest
# include ActionDispatch::Assertions::CapybaraAssertions
#
# test "can see the welcome page" do
# get "/"
# assert_css "h1", "Welcome#index"
# end
# end
#
# In addition to the assertions provided by <tt>Capybara::Minitest::Assertions</tt>, <tt>ActionDispatch::Assertions::CapybaraAssertions</tt> also declares a <tt>within</tt> test helper to change the current scope and a <tt>page</tt> test helper to access the <tt>Capybara::Session</tt> directly.
#
# Mix the <tt>ActionDispatch::Assertions::CapybaraAssertions</tt> module into the <tt>ActionDispatch::IntegrationTest</tt> class to
# integrate with Capybara's selectors and assertions throughout your integration test suite:
#
# # test/test_helper.rb
#
# require "action_dispatch/testing/assertions/capybara"
#
# # …
#
# class ActionDispatch::IntegrationTest
# include ActionDispatch::Assertions::CapybaraAssertions
# end
#
module CapybaraAssertions
extend ActiveSupport::Concern

module IntegrationSessionExtensions
delegate :within, to: :page

# :nodoc:
def _mock_session
@_mock_session ||= page.driver.browser.rack_mock_session
end

# Access the RackTest-driven <tt>Capybara::Session</tt> instance
#
# Assertions provided by the <tt>Capybara::Minitest::Assertions</tt>
# will implicitly interact with the <tt>Capybara::Session</tt> instance
# returned by this method.
#
# # Asserts a button with the text "Submit" exists in the response
# body HTML content:
#
# assert_button "Submit"
def page
@page ||= Capybara::Session.new(:rack_test, @app)
end
end

included do
include Capybara::Minitest::Assertions

setup { integration_session.extend(IntegrationSessionExtensions) }
end

class_methods do
def test(...)
Capybara.using_wait_time(0) { super }
end
end
end
end
end
54 changes: 54 additions & 0 deletions actionpack/test/dispatch/assertions/capybara_assertions_test.rb
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require "abstract_unit"
require "action_dispatch/testing/assertions/capybara"

class CapybaraAssertionsTest < ActionDispatch::IntegrationTest
include ActionDispatch::Assertions::CapybaraAssertions

ROUTES = ActionDispatch::Routing::RouteSet.new
ROUTES.draw do
post "/", to: "capybara_assertions_test/renders#create"
end

APP = build_app(ROUTES)

def app
APP
end

class RendersController < ActionController::Base
def create
render inline: params[:template]
end
end

test "assert scoped within an element" do
post "/", params: { template: <<~HTML }
<header><h1>Header</h1></header>
<main><h1>Main</h1></main>
HTML

assert_selector "header h1", text: "Header"
assert_selector "main h1", text: "Main"

within "header" do
assert_selector "h1", text: "Header"
assert_no_selector "h1", text: "Main"
end

within "main" do
assert_no_selector "h1", text: "Header"
assert_selector "h1", text: "Main"
end
end

test "assert_select with Capybara instead of rails-dom-testing" do
post "/", params: { template: <<~HTML }
<label for="name">Name</label>
<select id="name"><option>First</option></select>
HTML

assert_select "Name", options: ["First"]
end
end
42 changes: 42 additions & 0 deletions guides/source/testing.md
Expand Up @@ -1156,6 +1156,48 @@ NOTE: Don't forget to call `follow_redirect!` if you plan to make subsequent req

Finally we can assert that our response was successful and our new article is readable on the page.

#### Asserting with Capybara

By default, assertions about the contents of the response's body are provided by [rails-dom-testing][].

Action Dispatch can also integrate with Capybara's [selectors][Capybara::Selector] and [assertions][Capybara::Minitest::Assertions] by including the `ActionDispatch::Assertions::CapybaraAssertions` module:

```ruby
require "test_helper"
require "action_dispatch/testing/assertions/capybara"

class BlogFlowTest < ActionDispatch::IntegrationTest
include ActionDispatch::Assertions::CapybaraAssertions

test "can see the welcome page" do
get "/"
assert_css "h1", "Welcome#index"
end
end
```

In addition to the assertions provided by [Capybara::Minitest::Assertions][], `ActionDispatch::Assertions::CapybaraAssertions` also declares a [within][Capybara::Session#within] test helper to change the current scope and a [page][Capybara::Session] test helper to access the `Capybara::Session` directly.

Mix the `ActionDispatch::Assertions::CapybaraAssertions` module into the `ActionDispatch::IntegrationTest` class to integrate with Capybara's selectors and assertions throughout your integration test suite:

```ruby
# test/test_helper.rb

require "action_dispatch/testing/assertions/capybara"

#

class ActionDispatch::IntegrationTest
include ActionDispatch::Assertions::CapybaraAssertions
end
```

[rails-dom-testing]: https://github.com/rails/rails-dom-testing
[Capybara::Selector]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Selector
[Capybara::Minitest::Assertions]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Minitest/Assertions
[Capybara::Session]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session
[Capybara::Session#within]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session#within-instance_method

#### Taking it further

We were able to successfully test a very small workflow for visiting our blog and creating a new article. If we wanted to take this further we could add tests for commenting, removing articles, or editing comments. Integration tests are a great place to experiment with all kinds of use cases for our applications.
Expand Down

0 comments on commit 10d5797

Please sign in to comment.