Skip to content

Commit

Permalink
ActionDispatch::Testing::TestResponse#parsed_body parse HTML with N…
Browse files Browse the repository at this point in the history
…okogiri

Prior to this commit, the only out-of-the-box parsing that
`ActionDispatch::Testing::TestResponse#parsed_body` supported was for
`application/json` requests. This meant that `response.body ==
response.parsed_body` for HTML requests.

```ruby
get "/posts"
response.content_type         # => "text/html; charset=utf-8"
response.parsed_body.class    # => Nokogiri::HTML5::Document
response.parsed_body.to_html  # => "<!DOCTYPE html>\n<html>\n..."
```

Using `parsed_body` for JSON requests supports `Hash#fetch`, `Hash#dig`,
and Ruby 3.2 destructuring assignment and pattern matching.

The introduction of [Nokogiri support for pattern
matching][nokogiri-pattern-matching] poses an opportunity to make assertions
about the structure of the HTML response.

On top of that, there is ongoing work to [introduce pattern matching
support in MiniTest][minitest-pattern-matching].

[nokogiri-pattern-matching]: sparklemotion/nokogiri#2523
[minitest-pattern-matching]: minitest/minitest#936
  • Loading branch information
seanpdoyle committed Jan 28, 2023
1 parent 6b30c3f commit ad79ed0
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 10 deletions.
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ PATH
actionpack (7.1.0.alpha)
actionview (= 7.1.0.alpha)
activesupport (= 7.1.0.alpha)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
Expand Down
14 changes: 13 additions & 1 deletion actionpack/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
* Change `ActionDispatch::Testing::TestResponse#parsed_body` to parse HTML as
a Nokogiri document

```ruby
get "/posts"
response.content_type # => "text/html; charset=utf-8"
response.parsed_body.class # => Nokogiri::HTML5::Document
response.parsed_body.to_html # => "<!DOCTYPE html>\n<html>\n..."
```

*Sean Doyle*

* Add HTTP::Request#route_uri_pattern that returns URI pattern of matched route.

*Joel Hawksley*, *Kate Higa*

* Add `ActionDispatch::AssumeSSL` middleware that can be turned on via `config.assume_ssl`.
It makes the application believe that all requests are arring over SSL. This is useful
It makes the application believe that all requests are arriving over SSL. This is useful
when proxying through a load balancer that terminates SSL, the forwarded request will appear
as though its HTTP instead of HTTPS to the application. This makes redirects and cookie
security target HTTP instead of HTTPS. This middleware makes the server assume that the
Expand Down
1 change: 1 addition & 0 deletions actionpack/actionpack.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Gem::Specification.new do |s|

s.add_dependency "activesupport", version

s.add_dependency "nokogiri", ">= 1.8.5"
s.add_dependency "rack", ">= 2.2.4"
s.add_dependency "rack-session", ">= 1.0.1"
s.add_dependency "rack-test", ">= 0.6.3"
Expand Down
6 changes: 6 additions & 0 deletions actionpack/lib/action_dispatch/testing/request_encoder.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "nokogiri"

module ActionDispatch
class RequestEncoder # :nodoc:
class IdentityEncoder
Expand All @@ -9,6 +11,9 @@ def encode_params(params); params; end
def response_parser; -> body { body }; end
end

# :nodoc:
HTMLResponseParser = defined?(::Nokogiri::HTML5) ? ::Nokogiri::HTML5 : ::Nokogiri::HTML

@encoders = { identity: IdentityEncoder.new }

attr_reader :response_parser
Expand Down Expand Up @@ -50,6 +55,7 @@ def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
@encoders[mime_name] = new(mime_name, param_encoder, response_parser)
end

register_encoder :html, response_parser: -> body { HTMLResponseParser.parse(body) }
register_encoder :json, response_parser: -> body { JSON.parse(body) }
end
end
18 changes: 9 additions & 9 deletions actionpack/lib/action_dispatch/testing/test_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ def self.from_response(response)
#
# ==== Examples
# get "/posts"
# response.content_type # => "text/html; charset=utf-8"
# response.parsed_body.class # => String
# response.parsed_body # => "<!DOCTYPE html>\n<html>\n..."
# response.content_type # => "text/html; charset=utf-8"
# response.parsed_body.class # => Nokogiri::HTML5::Document
# response.parsed_body.to_html # => "<!DOCTYPE html>\n<html>\n..."
#
# get "/posts.json"
# response.content_type # => "application/json; charset=utf-8"
# response.parsed_body.class # => Array
# response.parsed_body # => [{"id"=>42, "title"=>"Title"},...
# response.content_type # => "application/json; charset=utf-8"
# response.parsed_body.class # => Array
# response.parsed_body # => [{"id"=>42, "title"=>"Title"},...
#
# get "/posts/42.json"
# response.content_type # => "application/json; charset=utf-8"
# response.parsed_body.class # => Hash
# response.parsed_body # => {"id"=>42, "title"=>"Title"}
# response.content_type # => "application/json; charset=utf-8"
# response.parsed_body.class # => Hash
# response.parsed_body # => {"id"=>42, "title"=>"Title"}
def parsed_body
@parsed_body ||= response_parser.call(body)
end
Expand Down
11 changes: 11 additions & 0 deletions actionpack/test/dispatch/test_response_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,16 @@ def assert_response_code_range(range, predicate)

response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }')
assert_equal({ "foo" => "fighters" }, response.parsed_body)

response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "text/html" }, <<~HTML)
<html>
<head></head>
<body>
<div>Content</div>
</body>
</html>
HTML
assert_kind_of(Nokogiri::XML::Document, response.parsed_body)
assert_equal(response.parsed_body.at_xpath("/html/body/div").text, "Content")
end
end

0 comments on commit ad79ed0

Please sign in to comment.