Skip to content

Commit

Permalink
Add Capybara assertion support to Controller, Integration, Mailer, an…
Browse files Browse the repository at this point in the history
…d View tests

This commit introduces framework-specific configuration values:

* `config.action_controller.assertions` for `ActionController::TestCase`
* `config.action_dispatch.assertions` for `ActionDispatch::IntegrationTest`
* `config.action_mailer.assertions` for `ActionMailer::TestCase`
* `config.action_view.assertions` for `ActionView::TestCase`

When set to `:capybara`, those tests include framework-scoped
`CapybaraAssertions` modules that transitively include
`Capybara::Minitest::Assertions`, and define the required `#page` method
to parse the HTML into a `Capybara::Node::Simple` instance.

When set to `:rails_dom_testing`, those tests include a framework-scoped
`RailsDomTestingAssetrions` module to preserve the existing behavior.

They all default to `:rails_dom_testing`.
  • Loading branch information
seanpdoyle committed Jan 13, 2024
1 parent f7ac22a commit 9f061c7
Show file tree
Hide file tree
Showing 28 changed files with 657 additions and 179 deletions.
7 changes: 7 additions & 0 deletions actionmailer/CHANGELOG.md
@@ -1,3 +1,10 @@
* Introduce `config.action_mailer.html_assertions`

Adds support for testing with `Capybara::Minitest::Assertions` when set to `:capybara`.
Defaults to `Rails::Dom::Testing::Assertions` with `:rails_dom_testing`.

*Sean Doyle*

* Remove deprecated params via `:args` for `assert_enqueued_email_with`.

*Rafael Mendonça França*
Expand Down
19 changes: 19 additions & 0 deletions actionmailer/lib/action_mailer/railtie.rb
Expand Up @@ -9,6 +9,7 @@ module ActionMailer
class Railtie < Rails::Railtie # :nodoc:
config.action_mailer = ActiveSupport::OrderedOptions.new
config.action_mailer.preview_paths = []
config.action_mailer.html_assertions = :rails_dom_testing
config.eager_load_namespaces << ActionMailer

initializer "action_mailer.deprecator", before: :load_environment_config do |app|
Expand All @@ -19,6 +20,24 @@ class Railtie < Rails::Railtie # :nodoc:
ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
end

initializer "action_mailer.test_case" do |app|
html_assertions = app.config.action_mailer.delete(:html_assertions)

ActiveSupport.on_load(:action_mailer_test_case) do
case html_assertions
when :capybara
include ActionView::CapybaraAssertions
when :rails_dom_testing
include Rails::Dom::Testing::Assertions::SelectorAssertions
include Rails::Dom::Testing::Assertions::DomAssertions
when :none
# do nothing
else
raise ArgumentError.new("unrecognized value #{assertions.inspect} for config.action_mailer.html_assertions")
end
end
end

initializer "action_mailer.set_configs" do |app|
paths = app.config.paths
options = app.config.action_mailer
Expand Down
33 changes: 31 additions & 2 deletions actionmailer/lib/action_mailer/test_case.rb
Expand Up @@ -34,14 +34,13 @@ module Behavior

include ActiveSupport::Testing::ConstantLookup
include TestHelper
include Rails::Dom::Testing::Assertions::SelectorAssertions
include Rails::Dom::Testing::Assertions::DomAssertions

included do
class_attribute :_mailer_class
setup :initialize_test_deliveries
setup :set_expected_mail
teardown :restore_test_deliveries
attr_accessor :html_document
ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
end

Expand Down Expand Up @@ -83,6 +82,36 @@ def read_fixture(action)
IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
end

# Extract and parse the HTML part of a Mail instance and yield it to the block.
#
# If the Mail is multipart, extract and parse the +text/html+ part.
# Otherwise, extract and parse the body. By default, parse the last delivered Mail.
#
# UsersMailer.create(user).deliver_now
# within_html_part do
# assert_text "Welcome, #{user.email}"
# end
#
# mail = UsersMailer.create(user)
# within_html_part mail do
# assert_text "Welcome, #{user.email}"
# end
def within_html_part(mail = self.class.mailer_class.deliveries.last, html_version: nil, &block)
parser = Rails::Dom::Testing.html_document_fragment(html_version: html_version)
part = mail.html_part || mail

if Mime[:html].match?(part.mime_type)
html = part.body.raw_source
self.html_document = parser.parse(html)

html_document.yield_self(&block)
else
raise ArgumentError, "no HTML part in #{mail.inspect}"
end
ensure
self.html_document = nil
end

private
def initialize_test_deliveries
set_delivery_method :test
Expand Down
5 changes: 5 additions & 0 deletions actionmailer/test/abstract_unit.rb
Expand Up @@ -40,4 +40,9 @@ class ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
end

class ActionMailer::TestCase
include Rails::Dom::Testing::Assertions::SelectorAssertions
include Rails::Dom::Testing::Assertions::DomAssertions
end

require_relative "../../tools/test_common"
10 changes: 10 additions & 0 deletions actionmailer/test/assert_select_email_test.rb
Expand Up @@ -46,4 +46,14 @@ def test_assert_select_email_multipart
end
end
end

def test_assert_select_within_html_part
mail = AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: "foo bar")
within_html_part mail do |root|
assert_select root, "div" do
assert_select "p:first-child", "foo"
assert_select "p:last-child", "bar"
end
end
end
end
77 changes: 77 additions & 0 deletions actionmailer/test/capybara_assertions_test.rb
@@ -0,0 +1,77 @@
# frozen_string_literal: true

require "abstract_unit"

class ActionMailer::CapybaraAssertionsTest < ActionMailer::TestCase
class TestMailer < ActionMailer::Base
def test(options)
text, html = options.values_at(:text, :html)

mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format|
if text.present?
format.text { render plain: text }
end
if html.present?
format.html { render plain: html }
end
end
end
end

include ActionView::CapybaraAssertions

tests TestMailer

test "assertions from last HTML delivery" do
TestMailer.test(html: "<div><p>foo</p><p>bar</p></div>").deliver_now

within_html_part do
assert_css "div" do |div|
assert_css div, "p", text: "foo"
assert_css div, "p", text: "bar"
end
end
end

test "assertions from HTML instance" do
mail = TestMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: "ignored")

within_html_part mail do
assert_css "div" do |div|
assert_css div, "p", text: "foo"
assert_css div, "p", text: "bar"
end
end
end

test "assertions from last multi-part delivery" do
TestMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: "ignored").deliver_now

within_html_part do
assert_css "div" do |div|
assert_css div, "p", text: "foo"
assert_css div, "p", text: "bar"
end
end
end

test "assertions from multi-part instance" do
mail = TestMailer.test(html: "<div><p>foo</p><p>bar</p></div>")

within_html_part mail do
assert_css "div" do |div|
assert_css div, "p", text: "foo"
assert_css div, "p", text: "bar"
end
end
end

test "fails when no HTML part" do
mail = TestMailer.test(text: "ignored")

assert_raises ArgumentError, match: "no HTML part in #{mail.inspect}" do
within_html_part mail do
end
end
end
end
6 changes: 4 additions & 2 deletions actionpack/CHANGELOG.md
@@ -1,5 +1,7 @@
* Introduce `ActionDispatch::Assertions::CapybaraAssertions` to support
`Capybara` assertions in `ActionDispatch::IntegrationTest`
* Introduce `config.action_controller.html_assertions` and `config.action_dispatch.html_assertions`

Adds support for testing with `Capybara::Minitest::Assertions` when set to `:capybara`.
Defaults to `Rails::Dom::Testing::Assertions` with `:rails_dom_testing`.

*Sean Doyle*

Expand Down
17 changes: 16 additions & 1 deletion actionpack/lib/action_controller/railtie.rb
Expand Up @@ -13,6 +13,7 @@ class Railtie < Rails::Railtie # :nodoc:
config.action_controller.raise_on_open_redirects = false
config.action_controller.log_query_tags_around_actions = true
config.action_controller.wrap_parameters_by_default = false
config.action_controller.html_assertions = :rails_dom_testing

config.eager_load_namespaces << AbstractController
config.eager_load_namespaces << ActionController
Expand Down Expand Up @@ -83,7 +84,8 @@ class Railtie < Rails::Railtie # :nodoc:
:action_on_unpermitted_parameters,
:always_permitted_parameters,
:wrap_parameters_by_default,
:allow_deprecated_parameters_hash_equality
:allow_deprecated_parameters_hash_equality,
:html_assertions
)

filtered_options.each do |k, v|
Expand Down Expand Up @@ -139,8 +141,21 @@ class Railtie < Rails::Railtie # :nodoc:
end

initializer "action_controller.test_case" do |app|
html_assertions = app.config.action_controller.delete(:html_assertions)

ActiveSupport.on_load(:action_controller_test_case) do
ActionController::TestCase.executor_around_each_request = app.config.active_support.executor_around_test_case

case html_assertions
when :capybara
include ActionView::CapybaraAssertions
when :rails_dom_testing
include ActionView::RailsDomTestingAssertions
when :none
# do nothing
else
raise ArgumentError.new("unrecognized value #{assertions.inspect} for config.action_controller.html_assertions")
end
end
end
end
Expand Down
5 changes: 0 additions & 5 deletions actionpack/lib/action_controller/test_case.rb
Expand Up @@ -349,7 +349,6 @@ module Behavior
extend ActiveSupport::Concern
include ActionDispatch::TestProcess
include ActiveSupport::Testing::ConstantLookup
include Rails::Dom::Testing::Assertions

attr_reader :response, :request

Expand Down Expand Up @@ -649,10 +648,6 @@ def scrub_env!(env)
env
end

def document_root_element
html_document.root
end

def check_required_ivars
# Check for required instance variables so we can give an
# understandable error message.
Expand Down
18 changes: 18 additions & 0 deletions actionpack/lib/action_dispatch/railtie.rb
Expand Up @@ -27,6 +27,7 @@ class Railtie < Rails::Railtie # :nodoc:
config.action_dispatch.request_id_header = ActionDispatch::Constants::X_REQUEST_ID
config.action_dispatch.log_rescued_responses = true
config.action_dispatch.debug_exception_log_level = :fatal
config.action_dispatch.html_assertions = :rails_dom_testing

config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
Expand Down Expand Up @@ -70,5 +71,22 @@ class Railtie < Rails::Railtie # :nodoc:

ActionDispatch.test_app = app
end

initializer "action_dispatch.integration_test" do |app|
html_assertions = app.config.action_dispatch.delete(:html_assertions)

ActiveSupport.on_load(:action_dispatch_integration_test) do
case html_assertions
when :capybara
include ActionView::CapybaraAssertions
when :rails_dom_testing
include ActionView::RailsDomTestingAssertions
when :none
# do nothing
else
raise ArgumentError.new("unrecognized value #{assertions.inspect} for config.action_dispatch.html_assertions")
end
end
end
end
end
1 change: 0 additions & 1 deletion actionpack/lib/action_dispatch/testing/assertions.rb
Expand Up @@ -10,7 +10,6 @@ module Assertions

include ResponseAssertions
include RoutingAssertions
include Rails::Dom::Testing::Assertions

def html_document
@html_document ||= if @response.media_type&.end_with?("xml")
Expand Down
81 changes: 0 additions & 81 deletions actionpack/lib/action_dispatch/testing/assertions/capybara.rb

This file was deleted.

0 comments on commit 9f061c7

Please sign in to comment.