From bec80373cd814f6bfb7d6779e68de02068a3cb10 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 4 Jan 2024 13:29:31 -0500 Subject: [PATCH] Parse `ActionView::TestCase#rendered` as DocumentFragment To integrate with [rails-dom-testing][] and its selector assertions, `ActionView::TestCase` [defines a `#document_root_element` method][document_root_element] that parses the HTML into a fully valid HTML document and returns the "root". In the case of most Action View partials rendered with `render partial: "..."`, the resulting document would be invalid, so its constituent parts (its ``, ``, and `` elements) are synthesized in during the parsing process. This results in a document whose _contents_ are equivalent to the original HTML string, but whose structure is not. To share a concrete example: ```ruby irb(main):002:0> rendered = "

Hello world

Goodbye world

" => "

Hello world

Goodbye world

" irb(main):003:0> root = Rails::Dom::Testing.html_document.parse(rendered).root => #(Element:0x57080 { ... irb(main):004:0> rendered.to_s => "

Hello world

Goodbye world

" irb(main):005:0> root.to_s => "

Hello world

Goodbye world

" irb(main):006:0> rendered.to_s == root.to_s => false ``` Prior to this commit, the parsed HTML content returned from calling `rendered.html` relied on the same mechanisms as `#document_root_element`, and parsed the HTML fragment into a full document, with a synthesized `` element as its root. The `rendered.html` value should reflect the content that was **rendered** by the partial, and should not behave the same as `#document_root_element`. When the parsing class is changed from [Nokogiri::XML::Document][] to [Nokogiri::XML::DocumentFragment][], the returned value reflects the same **exact** content as what was rendered. To elaborate on the previous example: ```ruby irb(main):007:0> fragment = Rails::Dom::Testing.html_document_fragment.parse(rendered) => #(DocumentFragment:0x62ee4 { ... irb(main):008:0> fragment.to_s => "

Hello world

Goodbye world

" irb(main):009:0> rendered.to_s == fragment.to_s => true ``` This commit changes the default `rendered.html` behavior to rely on `Nokogiri::XML::DocumentFragment` instead of `Nokogiri::XML::Document`. [Nokogiri::XML::Document]: https://nokogiri.org/rdoc/Nokogiri/XML/Document.html [Nokogiri::XML::DocumentFragment]: https://nokogiri.org/rdoc/Nokogiri/XML/DocumentFragment.html [document_root_element]: https://github.com/rails/rails-dom-testing/blob/v2.2.0/lib/rails/dom/testing/assertions/selector_assertions.rb#L75 --- actionview/CHANGELOG.md | 4 ++++ actionview/lib/action_view/test_case.rb | 2 +- actionview/test/template/test_case_test.rb | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index ffaa78af45373..2117a0bac941a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,7 @@ +* Parse `ActionView::TestCase#rendered` HTML content as `Nokogiri::XML::DocumentFragment` instead of `Nokogiri::XML::Document` + + *Sean Doyle* + * Rename `ActionView::TestCase::Behavior::{Content,RenderedViewContent}` *Sean Doyle* diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index a5a026efbadad..74f5fd83234e2 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -202,7 +202,7 @@ def include_helper_modules! setup :setup_with_controller - register_parser :html, -> rendered { Rails::Dom::Testing.html_document.parse(rendered).root } + register_parser :html, -> rendered { Rails::Dom::Testing.html_document_fragment.parse(rendered) } register_parser :json, -> rendered { JSON.parse(rendered, object_class: ActiveSupport::HashWithIndifferentAccess) } ActiveSupport.run_load_hooks(:action_view_test_case, self) diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 6e8ad38f8e16a..163ec700d2255 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -397,12 +397,13 @@ class RenderedViewContentTest < ActionView::TestCase end class HTMLParserTest < ActionView::TestCase - test "rendered.html is a Nokogiri::XML::Element" do + test "rendered.html is a Nokogiri::XML::DocumentFragment" do developer = DeveloperStruct.new("Eloy") render "developers/developer", developer: developer - assert_kind_of Nokogiri::XML::Element, rendered.html + assert_kind_of Nokogiri::XML::DocumentFragment, rendered.html + assert_equal rendered.to_s, rendered.html.to_s assert_equal developer.name, document_root_element.text end @@ -425,6 +426,7 @@ class JSONParserTest < ActionView::TestCase render formats: :json, partial: "developers/developer", locals: { developer: developer } assert_kind_of ActiveSupport::HashWithIndifferentAccess, rendered.json + assert_equal rendered.to_s, rendered.json.to_json assert_equal developer.name, rendered.json[:name] end end