Skip to content

Allow user to choose the HTML parser used #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .rdoc_options
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
encoding: UTF-8
charset: UTF-8
exclude:
- "\\.gemspec\\z"
- "gemfiles"
- "Gemfile*"
- "Rakefile"
main_page: "README.md"
markup: rdoc
59 changes: 39 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,6 @@ Doms are compared via `assert_dom_equal` and `assert_dom_not_equal`.
Elements are asserted via `assert_dom`, `assert_dom_encoded`, `assert_dom_email` and a subset of the dom can be selected with `css_select`.
The gem is developed for Rails 4.2 and above, and will not work on previous versions.

## Nokogiri::CSS::SyntaxError exceptions when upgrading to Rails 4.2:

Nokogiri is slightly stricter about the format of CSS selectors than the previous implementation.

Check the 4.2 release notes [section on `assert_select`](http://edgeguides.rubyonrails.org/4_2_release_notes.html#assert-select) for help.

## Installation

Add this line to your application's Gemfile:

gem 'rails-dom-testing'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rails-dom-testing

## Usage

### Dom Assertions
Expand Down Expand Up @@ -53,6 +33,45 @@ assert_dom_email '#you-got-mail'

The documentation in [selector_assertions.rb](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb) goes into a lot more detail of how selector assertions can be used.

### HTML versions

By default, assertions will use Nokogiri's HTML4 parser.

If `Rails::Dom::Testing.default_html_version` is set to `:html5`, then the assertions will use
Nokogiri's HTML5 parser. (If the HTML5 parser is not available on your platform, then a
`NotImplementedError` will be raised.)

When testing in a Rails application, the parser default can also be set by setting
`Rails.application.config.dom_testing_default_html_version`.

Some assertions support an `html_version:` keyword argument which can override the default for that
assertion. For example:

``` ruby
# compare DOMs built with the HTML5 parser
assert_dom_equal(expected, actual, html_version: :html5)

# compare DOMs built with the HTML4 parser
assert_dom_not_equal(expected, actual, html_version: :html4)
```

Please see documentation for individual assertions for more details.


## Installation

Add this line to your application's Gemfile:

gem 'rails-dom-testing'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rails-dom-testing

## Read more

Under the hood the doms are parsed with Nokogiri, and you'll generally be working with these two classes:
Expand Down
64 changes: 56 additions & 8 deletions lib/rails/dom/testing/assertions/dom_assertions.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative "../parser_selection"

module Rails
module Dom
module Testing
Expand All @@ -8,19 +10,65 @@ module DomAssertions
# \Test two HTML strings for equivalency (e.g., equal even when attributes are in another order)
#
# # assert that the referenced method generates the appropriate HTML string
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
def assert_dom_equal(expected, actual, message = nil, strict: false)
expected_dom, actual_dom = fragment(expected), fragment(actual)
# assert_dom_equal(
# '<a href="http://www.example.com">Apples</a>',
# link_to("Apples", "http://www.example.com"),
# )
#
# By default, the matcher will not pay attention to whitespace in text nodes (e.g., spaces
# and newlines). If you want stricter matching with exact matching for whitespace, pass
# <tt>strict: true</tt>:
#
# # these assertions will both pass
# assert_dom_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: false
# assert_dom_not_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: true
#
# The DOMs are created using an HTML parser specified by
# Rails::Dom::Testing.default_html_version (either :html4 or :html5).
#
# When testing in a Rails application, the parser default can also be set by setting
# +Rails.application.config.dom_testing_default_html_version+.
#
# If you want to specify the HTML parser just for a particular assertion, pass
# <tt>html_version: :html4</tt> or <tt>html_version: :html5</tt> keyword arguments:
#
# assert_dom_equal expected, actual, html_version: :html5
#
def assert_dom_equal(expected, actual, message = nil, strict: false, html_version: nil)
expected_dom, actual_dom = fragment(expected, html_version: html_version), fragment(actual, html_version: html_version)
message ||= "Expected: #{expected}\nActual: #{actual}"
assert compare_doms(expected_dom, actual_dom, strict), message
end

# The negated form of +assert_dom_equal+.
#
# # assert that the referenced method does not generate the specified HTML string
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
def assert_dom_not_equal(expected, actual, message = nil, strict: false)
expected_dom, actual_dom = fragment(expected), fragment(actual)
# assert_dom_not_equal(
# '<a href="http://www.example.com">Apples</a>',
# link_to("Oranges", "http://www.example.com"),
# )
#
# By default, the matcher will not pay attention to whitespace in text nodes (e.g., spaces
# and newlines). If you want stricter matching with exact matching for whitespace, pass
# <tt>strict: true</tt>:
#
# # these assertions will both pass
# assert_dom_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: false
# assert_dom_not_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: true
#
# The DOMs are created using an HTML parser specified by
# Rails::Dom::Testing.default_html_version (either :html4 or :html5).
#
# When testing in a Rails application, the parser default can also be set by setting
# +Rails.application.config.dom_testing_default_html_version+.
#
# If you want to specify the HTML parser just for a particular assertion, pass
# <tt>html_version: :html4</tt> or <tt>html_version: :html5</tt> keyword arguments:
#
# assert_dom_not_equal expected, actual, html_version: :html5
#
def assert_dom_not_equal(expected, actual, message = nil, strict: false, html_version: nil)
expected_dom, actual_dom = fragment(expected, html_version: html_version), fragment(actual, html_version: html_version)
message ||= "Expected: #{expected}\nActual: #{actual}"
assert_not compare_doms(expected_dom, actual_dom, strict), message
end
Expand Down Expand Up @@ -84,8 +132,8 @@ def equal_attribute?(attr, other_attr)
end

private
def fragment(text)
Nokogiri::HTML::DocumentFragment.parse(text)
def fragment(text, html_version: nil)
Rails::Dom::Testing.html_document_fragment(html_version: html_version).parse(text)
end
end
end
Expand Down
63 changes: 49 additions & 14 deletions lib/rails/dom/testing/assertions/selector_assertions.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require_relative "../parser_selection"
require_relative "selector_assertions/count_describable"
require_relative "selector_assertions/html_selector"

Expand Down Expand Up @@ -212,7 +213,25 @@ def assert_dom(*args, &block)
# end
# end
# end
def assert_dom_encoded(element = nil, &block)
#
# The DOM is created using an HTML parser specified by
# Rails::Dom::Testing.default_html_version (either :html4 or :html5).
#
# When testing in a Rails application, the parser default can also be set by setting
# +Rails.application.config.dom_testing_default_html_version+.
#
# If you want to specify the HTML parser just for a particular assertion, pass
# <tt>html_version: :html4</tt> or <tt>html_version: :html5</tt> keyword arguments:
#
# assert_dom "feed[xmlns='http://www.w3.org/2005/Atom']" do
# assert_dom "entry>title" do
# assert_dom_encoded(html_version: :html5) do
# assert_dom "b"
# end
# end
# end
#
def assert_dom_encoded(element = nil, html_version: nil, &block)
if !element && !@selected
raise ArgumentError, "Element is required when called from a nonnested assert_dom"
end
Expand All @@ -223,7 +242,7 @@ def assert_dom_encoded(element = nil, &block)
end.map(&:content)
end.join

selected = Nokogiri::HTML::DocumentFragment.parse(content)
selected = Rails::Dom::Testing.html_document_fragment(html_version: html_version).parse(content)
nest_selection(selected) do
if content.empty?
yield selected
Expand All @@ -239,24 +258,40 @@ def assert_dom_encoded(element = nil, &block)
# You must enable deliveries for this assertion to work, use:
# ActionMailer::Base.perform_deliveries = true
#
# assert_dom_email do
# assert_dom "h1", "Email alert"
# end
#
# assert_dom_email do
# items = assert_dom "ol>li"
# items.each do
# # Work with items here...
# end
# end
def assert_dom_email(&block)
# Example usage:
#
# assert_dom_email do
# assert_dom "h1", "Email alert"
# end
#
# assert_dom_email do
# items = assert_dom "ol>li"
# items.each do
# # Work with items here...
# end
# end
#
# The DOM is created using an HTML parser specified by
# Rails::Dom::Testing.default_html_version (either :html4 or :html5).
#
# When testing in a Rails application, the parser default can also be set by setting
# +Rails.application.config.dom_testing_default_html_version+.
#
# If you want to specify the HTML parser just for a particular assertion, pass
# <tt>html_version: :html4</tt> or <tt>html_version: :html5</tt> keyword arguments:
#
# assert_dom_email(html_version: :html5) do
# assert_dom "h1", "Email alert"
# end
#
def assert_dom_email(html_version: nil, &block)
deliveries = ActionMailer::Base.deliveries
assert !deliveries.empty?, "No e-mail in delivery list"

deliveries.each do |delivery|
(delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
if /^text\/html\W/.match?(part["Content-Type"].to_s)
root = Nokogiri::HTML::DocumentFragment.parse(part.body.to_s)
root = Rails::Dom::Testing.html_document_fragment(html_version: html_version).parse(part.body.to_s)
assert_dom root, ":root", &block
end
end
Expand Down
51 changes: 51 additions & 0 deletions lib/rails/dom/testing/parser_selection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require "active_support"
require "active_support/core_ext/module/attribute_accessors"

module Rails
module Dom
module Testing
mattr_accessor :default_html_version, default: :html4

class << self
def html5_support?
defined?(Nokogiri::HTML5)
end

def html_document(html_version: nil)
parser_classes = { html4: Nokogiri::HTML4::Document }
parser_classes[:html5] = Nokogiri::HTML5::Document if html5_support?

choose_html_parser(parser_classes, html_version: html_version)
end

def html_document_fragment(html_version: nil)
parser_classes = { html4: Nokogiri::HTML4::DocumentFragment }
parser_classes[:html5] = Nokogiri::HTML5::DocumentFragment if html5_support?

choose_html_parser(parser_classes, html_version: html_version)
end

private
def choose_html_parser(parser_classes, html_version: nil)
html_version ||= Rails::Dom::Testing.default_html_version

case html_version
when :html4
parser_classes[:html4]
when :html5
unless Rails::Dom::Testing.html5_support?
raise NotImplementedError, "html5 parser is not supported on this platform"
end
parser_classes[:html5]
else
raise ArgumentError, "html_version must be :html4 or :html5, received #{html_version.inspect}"
end
end
end
end
end
end

require_relative "railtie" if defined?(Rails::Railtie)
14 changes: 14 additions & 0 deletions lib/rails/dom/testing/railtie.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Rails
module Dom
module Testing
class Railtie < Rails::Railtie # :nodoc:
config.after_initialize do |app|
version = app.config.try(:dom_testing_default_html_version) # Rails 7.1+
Rails::Dom::Testing.default_html_version = version if version
end
end
end
end
end
Loading