`ActionDispatch::Testing::TestResponse#parsed_body` parse HTML with Nokogiri

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]: https://github.com/sparklemotion/nokogiri/pull/2523
[minitest-pattern-matching]: https://github.com/minitest/minitest/pull/936
This commit is contained in:
Sean Doyle 2023-01-25 16:20:33 -05:00
parent 6b30c3f410
commit ad79ed0e6b
6 changed files with 41 additions and 10 deletions

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require "nokogiri"
module ActionDispatch
class RequestEncoder # :nodoc:
class IdentityEncoder
@ -9,6 +11,9 @@ module ActionDispatch
def response_parser; -> body { body }; end
end
# :nodoc:
HTMLResponseParser = defined?(::Nokogiri::HTML5) ? ::Nokogiri::HTML5 : ::Nokogiri::HTML
@encoders = { identity: IdentityEncoder.new }
attr_reader :response_parser
@ -50,6 +55,7 @@ module ActionDispatch
@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

View File

@ -19,19 +19,19 @@ module ActionDispatch
#
# ==== 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

View File

@ -26,5 +26,16 @@ class TestResponseTest < ActiveSupport::TestCase
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