Merge pull request #49194 from seanpdoyle/action-view-test-case-encoder
Introduce `ActionView::TestCase.register_parser`
This commit is contained in:
commit
7893e0a165
@ -1,3 +1,21 @@
|
||||
* Introduce `ActionView::TestCase.register_parser`
|
||||
|
||||
```ruby
|
||||
register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }
|
||||
|
||||
test "renders RSS" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render formats: :rss, partial: article
|
||||
|
||||
assert_equal "Hello, world", rendered.rss.items.last.title
|
||||
end
|
||||
```
|
||||
|
||||
By default, register parsers for `:html` and `:json`.
|
||||
|
||||
*Sean Doyle*
|
||||
|
||||
## Rails 7.1.0.beta1 (September 13, 2023) ##
|
||||
|
||||
* Fix `simple_format` with blank `wrapper_tag` option returns plain html tag
|
||||
|
@ -60,9 +60,96 @@ module Behavior
|
||||
include ActiveSupport::Testing::ConstantLookup
|
||||
|
||||
delegate :lookup_context, to: :controller
|
||||
attr_accessor :controller, :request, :output_buffer, :rendered
|
||||
attr_accessor :controller, :request, :output_buffer
|
||||
|
||||
module ClassMethods
|
||||
def inherited(descendant) # :nodoc:
|
||||
super
|
||||
|
||||
descendant_content_class = content_class.dup
|
||||
|
||||
if descendant_content_class.respond_to?(:set_temporary_name)
|
||||
descendant_content_class.set_temporary_name("rendered_content")
|
||||
end
|
||||
|
||||
descendant.content_class = descendant_content_class
|
||||
end
|
||||
|
||||
# Register a callable to parse rendered content for a given template
|
||||
# format.
|
||||
#
|
||||
# Each registered parser will also define a +#rendered.[FORMAT]+ helper
|
||||
# method, where +[FORMAT]+ corresponds to the value of the
|
||||
# +format+ argument.
|
||||
#
|
||||
# === Arguments
|
||||
#
|
||||
# <tt>format</tt> - Symbol the name of the format used to render view's content
|
||||
# <tt>callable</tt> - Callable to parse the String. Accepts the String.
|
||||
# value as its only argument.
|
||||
# <tt>block</tt> - Block serves as the parser when the
|
||||
# <tt>callable</tt> is omitted.
|
||||
#
|
||||
# By default, ActionView::TestCase defines a parser for:
|
||||
#
|
||||
# * :html - returns an instance of Nokogiri::XML::Node
|
||||
# * :json - returns an instance of ActiveSupport::HashWithIndifferentAccess
|
||||
#
|
||||
# Each pre-registered parser also defines a corresponding helper:
|
||||
#
|
||||
# * :html - defines `rendered.html`
|
||||
# * :json - defines `rendered.json`
|
||||
#
|
||||
# === Examples
|
||||
#
|
||||
# test "renders HTML" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render partial: "articles/article", locals: { article: article }
|
||||
#
|
||||
# assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
|
||||
# end
|
||||
#
|
||||
# test "renders JSON" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render formats: :json, partial: "articles/article", locals: { article: article }
|
||||
#
|
||||
# assert_pattern { rendered.json => { title: "Hello, world" } }
|
||||
# end
|
||||
#
|
||||
# To parse the rendered content into RSS, register a call to <tt>RSS::Parser.parse</tt>:
|
||||
#
|
||||
# register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }
|
||||
#
|
||||
# test "renders RSS" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render formats: :rss, partial: article
|
||||
#
|
||||
# assert_equal "Hello, world", rendered.rss.items.last.title
|
||||
# end
|
||||
#
|
||||
# To parse the rendered content into a Capybara::Simple::Node,
|
||||
# re-register an <tt>:html</tt> parser with a call to
|
||||
# <tt>Capybara.string</tt>:
|
||||
#
|
||||
# register_parser :html, -> rendered { Capybara.string(rendered) }
|
||||
#
|
||||
# test "renders HTML" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render partial: article
|
||||
#
|
||||
# rendered.html.assert_css "h1", text: "Hello, world"
|
||||
# end
|
||||
def register_parser(format, callable = nil, &block)
|
||||
parser = callable || block || :itself.to_proc
|
||||
content_class.redefine_method(format) do
|
||||
parser.call(to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def tests(helper_class)
|
||||
case helper_class
|
||||
when String, Symbol
|
||||
@ -108,6 +195,27 @@ def include_helper_modules!
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
class_attribute :content_class, instance_accessor: false, default: Content
|
||||
|
||||
setup :setup_with_controller
|
||||
|
||||
register_parser :html, -> rendered { Rails::Dom::Testing.html_document.parse(rendered).root }
|
||||
register_parser :json, -> rendered { JSON.parse(rendered, object_class: ActiveSupport::HashWithIndifferentAccess) }
|
||||
|
||||
ActiveSupport.run_load_hooks(:action_view_test_case, self)
|
||||
|
||||
helper do
|
||||
def protect_against_forgery?
|
||||
false
|
||||
end
|
||||
|
||||
def _test_case
|
||||
controller._test_case
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def setup_with_controller
|
||||
controller_class = Class.new(ActionView::TestCase::TestController)
|
||||
@controller = controller_class.new
|
||||
@ -134,10 +242,64 @@ def rendered_views
|
||||
@_rendered_views ||= RenderedViewsCollection.new
|
||||
end
|
||||
|
||||
# Returns the content rendered by the last +render+ call.
|
||||
#
|
||||
# The returned object behaves like a string but also exposes a number of methods
|
||||
# that allows you to parse the content string in formats registered using
|
||||
# <tt>.register_parser</tt>.
|
||||
#
|
||||
# By default includes the following parsers:
|
||||
#
|
||||
# +.html+
|
||||
#
|
||||
# Parse the <tt>rendered</tt> content String into HTML. By default, this means
|
||||
# a <tt>Nokogiri::XML::Node</tt>.
|
||||
#
|
||||
# test "renders HTML" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render partial: "articles/article", locals: { article: article }
|
||||
#
|
||||
# assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
|
||||
# end
|
||||
#
|
||||
# To parse the rendered content into a <tt>Capybara::Simple::Node</tt>,
|
||||
# re-register an <tt>:html</tt> parser with a call to
|
||||
# <tt>Capybara.string</tt>:
|
||||
#
|
||||
# register_parser :html, -> rendered { Capybara.string(rendered) }
|
||||
#
|
||||
# test "renders HTML" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render partial: article
|
||||
#
|
||||
# rendered.html.assert_css "h1", text: "Hello, world"
|
||||
# end
|
||||
#
|
||||
# +.json+
|
||||
#
|
||||
# Parse the <tt>rendered</tt> content String into JSON. By default, this means
|
||||
# a <tt>ActiveSupport::HashWithIndifferentAccess</tt>.
|
||||
#
|
||||
# test "renders JSON" do
|
||||
# article = Article.create!(title: "Hello, world")
|
||||
#
|
||||
# render formats: :json, partial: "articles/article", locals: { article: article }
|
||||
#
|
||||
# assert_pattern { rendered.json => { title: "Hello, world" } }
|
||||
# end
|
||||
def rendered
|
||||
@_rendered ||= self.class.content_class.new(@rendered)
|
||||
end
|
||||
|
||||
def _routes
|
||||
@controller._routes if @controller.respond_to?(:_routes)
|
||||
end
|
||||
|
||||
class Content < SimpleDelegator
|
||||
end
|
||||
|
||||
# Need to experiment if this priority is the best one: rendered => output_buffer
|
||||
class RenderedViewsCollection
|
||||
def initialize
|
||||
@ -164,21 +326,6 @@ def view_rendered?(view, expected_locals)
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
setup :setup_with_controller
|
||||
ActiveSupport.run_load_hooks(:action_view_test_case, self)
|
||||
|
||||
helper do
|
||||
def protect_against_forgery?
|
||||
false
|
||||
end
|
||||
|
||||
def _test_case
|
||||
controller._test_case
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Need to experiment if this priority is the best one: rendered => output_buffer
|
||||
def document_root_element
|
||||
|
1
actionview/test/fixtures/developers/_developer.json.ruby
vendored
Normal file
1
actionview/test/fixtures/developers/_developer.json.ruby
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ name: developer.name }.to_json
|
@ -382,6 +382,65 @@ def page
|
||||
end
|
||||
end
|
||||
|
||||
class RenderedMethodMissingTest < ActionView::TestCase
|
||||
test "rendered delegates methods to the String" do
|
||||
developer = DeveloperStruct.new("Eloy")
|
||||
|
||||
render "developers/developer", developer: developer
|
||||
|
||||
assert_kind_of String, rendered.to_s
|
||||
assert_equal developer.name, rendered
|
||||
assert_match rendered, /#{developer.name}/
|
||||
assert_includes rendered, developer.name
|
||||
end
|
||||
end
|
||||
|
||||
class HTMLParserTest < ActionView::TestCase
|
||||
test "rendered.html is a Nokogiri::XML::Element" do
|
||||
developer = DeveloperStruct.new("Eloy")
|
||||
|
||||
render "developers/developer", developer: developer
|
||||
|
||||
assert_kind_of Nokogiri::XML::Element, rendered.html
|
||||
assert_equal developer.name, document_root_element.text
|
||||
end
|
||||
|
||||
test "do not memoize the rendered.html in view tests" do
|
||||
concat form_tag("/foo")
|
||||
|
||||
assert_equal "/foo", document_root_element.at("form")["action"]
|
||||
|
||||
concat content_tag(:b, "Strong", class: "foo")
|
||||
|
||||
assert_equal "/foo", document_root_element.at("form")["action"]
|
||||
assert_equal "foo", document_root_element.at("b")["class"]
|
||||
end
|
||||
end
|
||||
|
||||
class JSONParserTest < ActionView::TestCase
|
||||
test "rendered.json is an ActiveSupport::HashWithIndifferentAccess" do
|
||||
developer = DeveloperStruct.new("Eloy")
|
||||
|
||||
render formats: :json, partial: "developers/developer", locals: { developer: developer }
|
||||
|
||||
assert_kind_of ActiveSupport::HashWithIndifferentAccess, rendered.json
|
||||
assert_equal developer.name, rendered.json[:name]
|
||||
end
|
||||
end
|
||||
|
||||
class MissingHTMLParserTest < ActionView::TestCase
|
||||
register_parser :html, nil
|
||||
|
||||
test "rendered.html falls back to returning the value when the parser is missing" do
|
||||
developer = DeveloperStruct.new("Eloy")
|
||||
|
||||
render "developers/developer", developer: developer
|
||||
|
||||
assert_kind_of String, rendered.html
|
||||
assert_equal developer.name, rendered.html
|
||||
end
|
||||
end
|
||||
|
||||
module AHelperWithInitialize
|
||||
def initialize(*)
|
||||
super
|
||||
|
@ -9,6 +9,27 @@ class ActionView::PatternMatchingTestCases < ActionView::TestCase
|
||||
# rubocop:disable Lint/Syntax
|
||||
assert_pattern { document_root_element.at("h1") => { content: "Eloy", attributes: [{ name: "id", value: "name" }] } }
|
||||
refute_pattern { document_root_element.at("h1") => { content: "Not Eloy" } }
|
||||
end
|
||||
|
||||
test "rendered.html integrates with pattern matching" do
|
||||
developer = DeveloperStruct.new("Eloy")
|
||||
|
||||
render "developers/developer", developer: developer
|
||||
|
||||
# rubocop:disable Lint/Syntax
|
||||
assert_pattern { rendered.html => { content: "Eloy" } }
|
||||
refute_pattern { rendered.html => { content: "Not Eloy" } }
|
||||
# rubocop:enable Lint/Syntax
|
||||
end
|
||||
|
||||
test "rendered.json integrates with pattern matching" do
|
||||
developer = DeveloperStruct.new("Eloy")
|
||||
|
||||
render formats: :json, partial: "developers/developer", locals: { developer: developer }
|
||||
|
||||
# rubocop:disable Lint/Syntax
|
||||
assert_pattern { rendered.json => { name: "Eloy" } }
|
||||
refute_pattern { rendered.json => { name: "Not Eloy" } }
|
||||
# rubocop:enable Lint/Syntax
|
||||
end
|
||||
end
|
||||
|
@ -422,6 +422,62 @@ assert_pattern { html.at("main") => { children: [{ name: "h1", content: /content
|
||||
[nokogiri-pattern-matching]: https://nokogiri.org/rdoc/Nokogiri/XML/Attr.html#method-i-deconstruct_keys
|
||||
[minitest-pattern-matching]: https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_pattern
|
||||
|
||||
### Introduce `ActionView::TestCase.register_parser`
|
||||
|
||||
[Extend `ActionView::TestCase`][#49194] to support parsing content rendered by
|
||||
view partials into known structures. By default, define `rendered_html` to parse
|
||||
HTML into a `Nokogiri::XML::Node` and `rendered_json` to parse JSON into an
|
||||
`ActiveSupport::HashWithIndifferentAccess`:
|
||||
|
||||
```ruby
|
||||
test "renders HTML" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render partial: "articles/article", locals: { article: article }
|
||||
|
||||
assert_pattern { rendered_html.at("main h1") => { content: "Hello, world" } }
|
||||
end
|
||||
|
||||
test "renders JSON" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render formats: :json, partial: "articles/article", locals: { article: article }
|
||||
|
||||
assert_pattern { rendered_json => { title: "Hello, world" } }
|
||||
end
|
||||
```
|
||||
|
||||
To parse the rendered content into RSS, register a call to `RSS::Parser.parse`:
|
||||
|
||||
```ruby
|
||||
register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }
|
||||
|
||||
test "renders RSS" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render formats: :rss, partial: article, locals: { article: article }
|
||||
|
||||
assert_equal "Hello, world", rendered_rss.items.last.title
|
||||
end
|
||||
```
|
||||
|
||||
To parse the rendered content into a Capybara::Simple::Node, re-register an
|
||||
`:html` parser with a call to `Capybara.string`:
|
||||
|
||||
```ruby
|
||||
register_parser :html, -> rendered { Capybara.string(rendered) }
|
||||
|
||||
test "renders HTML" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render partial: article
|
||||
|
||||
rendered_html.assert_css "main h1", text: "Hello, world"
|
||||
end
|
||||
```
|
||||
|
||||
[#49194]: https://github.com/rails/rails/pull/49194
|
||||
|
||||
Railties
|
||||
--------
|
||||
|
||||
|
@ -1699,7 +1699,7 @@ Partial templates - usually called "partials" - are another device for breaking
|
||||
View tests provide an opportunity to test that partials render content the way you expect. View partial tests reside in `test/views/` and inherit from `ActionView::TestCase`.
|
||||
|
||||
To render a partial, call `render` like you would in a template. The content is
|
||||
available through the test-local `rendered` method:
|
||||
available through the test-local `#rendered` method:
|
||||
|
||||
```ruby
|
||||
class ArticlePartialTest < ActionView::TestCase
|
||||
@ -1793,7 +1793,52 @@ class ArticlePartialTest < ViewPartialTestCase
|
||||
end
|
||||
```
|
||||
|
||||
Starting in Action View version 7.1, the `#rendered` helper method returns an
|
||||
object capable of parsing the view partial's rendered content.
|
||||
|
||||
To transform the `String` content returned by the `#rendered` method into an
|
||||
object, define a parser by calling `.register_parser`. Calling
|
||||
`.register_parser :rss` defines a `#rendered.rss` helper method. For example,
|
||||
to parse rendered [RSS content][] into an object with `#rendered.rss`, register
|
||||
a call to `RSS::Parser.parse`:
|
||||
|
||||
```ruby
|
||||
register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }
|
||||
|
||||
test "renders RSS" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render formats: :rss, partial: article
|
||||
|
||||
assert_equal "Hello, world", rendered.rss.items.last.title
|
||||
end
|
||||
```
|
||||
|
||||
By default, `ActionView::TestCase` defines a parser for:
|
||||
|
||||
* `:html` - returns an instance of [Nokogiri::XML::Node](https://nokogiri.org/rdoc/Nokogiri/XML/Node.html)
|
||||
* `:json` - returns an instance of [ActiveSupport::HashWithIndifferentAccess](https://edgeapi.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html)
|
||||
|
||||
```ruby
|
||||
test "renders HTML" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render partial: "articles/article", locals: { article: article }
|
||||
|
||||
assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
|
||||
end
|
||||
|
||||
test "renders JSON" do
|
||||
article = Article.create!(title: "Hello, world")
|
||||
|
||||
render formats: :json, partial: "articles/article", locals: { article: article }
|
||||
|
||||
assert_pattern { rendered.json => { title: "Hello, world" } }
|
||||
end
|
||||
```
|
||||
|
||||
[rails-dom-testing]: https://github.com/rails/rails-dom-testing
|
||||
[RSS content]: https://www.rssboard.org/rss-specification
|
||||
|
||||
Testing Helpers
|
||||
---------------
|
||||
|
Loading…
Reference in New Issue
Block a user