Skip to content

Commit 8355a66

Browse files
committed
Reanimate assert_tag. The thriller of the summer: It came from the git history!
1 parent 3c78936 commit 8355a66

File tree

5 files changed

+330
-0
lines changed

5 files changed

+330
-0
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
source 'https://rubygems.org'
22

3+
gem 'rails', github: 'rails/rails'
4+
35
# Specify your gem's dependencies in rails-dom-testing.gemspec
46
gemspec

lib/rails/dom/testing/assertions.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ module Testing
77
module Assertions
88
autoload :DomAssertions, 'rails/dom/testing/assertions/dom_assertions'
99
autoload :SelectorAssertions, 'rails/dom/testing/assertions/selector_assertions'
10+
autoload :TagAssertions, 'rails/dom/testing/assertions/tag_assertions'
1011

1112
extend ActiveSupport::Concern
1213

1314
include DomAssertions
1415
include SelectorAssertions
16+
include TagAssertions
1517
end
1618
end
1719
end
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
require 'active_support/dependencies/autoload' # for html-scanner
2+
require 'rails/deprecated_sanitizer/html-scanner'
3+
4+
module Rails
5+
module Dom
6+
module Testing
7+
module Assertions
8+
# Pair of assertions to testing elements in the HTML output of the response.
9+
module TagAssertions
10+
# Asserts that there is a tag/node/element in the body of the response
11+
# that meets all of the given conditions. The +conditions+ parameter must
12+
# be a hash of any of the following keys (all are optional):
13+
#
14+
# * <tt>:tag</tt>: the node type must match the corresponding value
15+
# * <tt>:attributes</tt>: a hash. The node's attributes must match the
16+
# corresponding values in the hash.
17+
# * <tt>:parent</tt>: a hash. The node's parent must match the
18+
# corresponding hash.
19+
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
20+
# must meet the criteria described by the hash.
21+
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
22+
# meet the criteria described by the hash.
23+
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
24+
# must meet the criteria described by the hash.
25+
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
26+
# meet the criteria described by the hash.
27+
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
28+
# the criteria described by the hash, and at least one sibling must match.
29+
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
30+
# the criteria described by the hash, and at least one sibling must match.
31+
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts
32+
# the keys:
33+
# * <tt>:count</tt>: either a number or a range which must equal (or
34+
# include) the number of children that match.
35+
# * <tt>:less_than</tt>: the number of matching children must be less
36+
# than this number.
37+
# * <tt>:greater_than</tt>: the number of matching children must be
38+
# greater than this number.
39+
# * <tt>:only</tt>: another hash consisting of the keys to use
40+
# to match on the children, and only matching children will be
41+
# counted.
42+
# * <tt>:content</tt>: the textual content of the node must match the
43+
# given value. This will not match HTML tags in the body of a
44+
# tag--only text.
45+
#
46+
# Conditions are matched using the following algorithm:
47+
#
48+
# * if the condition is a string, it must be a substring of the value.
49+
# * if the condition is a regexp, it must match the value.
50+
# * if the condition is a number, the value must match number.to_s.
51+
# * if the condition is +true+, the value must not be +nil+.
52+
# * if the condition is +false+ or +nil+, the value must be +nil+.
53+
#
54+
# # Assert that there is a "span" tag
55+
# assert_tag tag: "span"
56+
#
57+
# # Assert that there is a "span" tag with id="x"
58+
# assert_tag tag: "span", attributes: { id: "x" }
59+
#
60+
# # Assert that there is a "span" tag using the short-hand
61+
# assert_tag :span
62+
#
63+
# # Assert that there is a "span" tag with id="x" using the short-hand
64+
# assert_tag :span, attributes: { id: "x" }
65+
#
66+
# # Assert that there is a "span" inside of a "div"
67+
# assert_tag tag: "span", parent: { tag: "div" }
68+
#
69+
# # Assert that there is a "span" somewhere inside a table
70+
# assert_tag tag: "span", ancestor: { tag: "table" }
71+
#
72+
# # Assert that there is a "span" with at least one "em" child
73+
# assert_tag tag: "span", child: { tag: "em" }
74+
#
75+
# # Assert that there is a "span" containing a (possibly nested)
76+
# # "strong" tag.
77+
# assert_tag tag: "span", descendant: { tag: "strong" }
78+
#
79+
# # Assert that there is a "span" containing between 2 and 4 "em" tags
80+
# # as immediate children
81+
# assert_tag tag: "span",
82+
# children: { count: 2..4, only: { tag: "em" } }
83+
#
84+
# # Get funky: assert that there is a "div", with an "ul" ancestor
85+
# # and an "li" parent (with "class" = "enum"), and containing a
86+
# # "span" descendant that contains text matching /hello world/
87+
# assert_tag tag: "div",
88+
# ancestor: { tag: "ul" },
89+
# parent: { tag: "li",
90+
# attributes: { class: "enum" } },
91+
# descendant: { tag: "span",
92+
# child: /hello world/ }
93+
#
94+
# <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
95+
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
96+
# (like br and hr and such) but will not work correctly with tags
97+
# that allow optional closing tags (p, li, td). <em>You must explicitly
98+
# close all of your tags to use these assertions.</em>
99+
def assert_tag(*opts)
100+
opts = opts.size > 1 ? opts.last.merge({ tag: opts.first.to_s }) : opts.first
101+
tag = find_tag(opts)
102+
103+
assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
104+
end
105+
106+
# Identical to +assert_tag+, but asserts that a matching tag does _not_
107+
# exist. (See +assert_tag+ for a full discussion of the syntax.)
108+
#
109+
# # Assert that there is not a "div" containing a "p"
110+
# assert_no_tag tag: "div", descendant: { tag: "p" }
111+
#
112+
# # Assert that an unordered list is empty
113+
# assert_no_tag tag: "ul", descendant: { tag: "li" }
114+
#
115+
# # Assert that there is not a "p" tag with between 1 to 3 "img" tags
116+
# # as immediate children
117+
# assert_no_tag tag: "p",
118+
# children: { count: 1..3, only: { tag: "img" } }
119+
def assert_no_tag(*opts)
120+
opts = opts.size > 1 ? opts.last.merge({ tag: opts.first.to_s }) : opts.first
121+
tag = find_tag(opts)
122+
123+
assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
124+
end
125+
126+
def find_tag(conditions)
127+
html_scanner_document.find(conditions)
128+
end
129+
130+
def find_all_tag(conditions)
131+
html_scanner_document.find_all(conditions)
132+
end
133+
134+
private
135+
def html_scanner_document
136+
xml = @response.content_type =~ /xml$/
137+
@html_document ||= HTML::Document.new(@response.body, false, xml)
138+
end
139+
end
140+
end
141+
end
142+
end
143+
end

rails-dom-testing.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
1919

2020
spec.add_dependency "nokogiri", "~> 1.6.0"
2121
spec.add_dependency "activesupport"
22+
spec.add_dependency "rails-deprecated_sanitizer"
2223

2324
spec.add_development_dependency "bundler", "~> 1.3"
2425
spec.add_development_dependency "rake"

test/tag_assertions_test.rb

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
require 'test_helper'
2+
require 'rails/dom/testing/assertions/tag_assertions'
3+
4+
HTML_TEST_OUTPUT = <<HTML
5+
<html>
6+
<body>
7+
<a href="/"><img src="/images/button.png" /></a>
8+
<div id="foo">
9+
<ul>
10+
<li class="item">hello</li>
11+
<li class="item">goodbye</li>
12+
</ul>
13+
</div>
14+
<div id="bar">
15+
<form action="/somewhere">
16+
Name: <input type="text" name="person[name]" id="person_name" />
17+
</form>
18+
</div>
19+
</body>
20+
</html>
21+
HTML
22+
23+
class AssertTagTest < ActiveSupport::TestCase
24+
include Rails::Dom::Testing::Assertions::TagAssertions
25+
26+
class FakeResponse
27+
attr_accessor :content_type, :body
28+
29+
def initialize(content_type, body)
30+
@content_type, @body = content_type, body
31+
end
32+
end
33+
34+
setup do
35+
@response = FakeResponse.new 'html', HTML_TEST_OUTPUT
36+
end
37+
38+
def test_assert_tag_tag
39+
# there is a 'form' tag
40+
assert_tag tag: 'form'
41+
# there is not an 'hr' tag
42+
assert_no_tag tag: 'hr'
43+
end
44+
45+
def test_assert_tag_attributes
46+
# there is a tag with an 'id' of 'bar'
47+
assert_tag attributes: { id: "bar" }
48+
# there is no tag with a 'name' of 'baz'
49+
assert_no_tag attributes: { name: "baz" }
50+
end
51+
52+
def test_assert_tag_parent
53+
# there is a tag with a parent 'form' tag
54+
assert_tag parent: { tag: "form" }
55+
# there is no tag with a parent of 'input'
56+
assert_no_tag parent: { tag: "input" }
57+
end
58+
59+
def test_assert_tag_child
60+
# there is a tag with a child 'input' tag
61+
assert_tag child: { tag: "input" }
62+
# there is no tag with a child 'strong' tag
63+
assert_no_tag child: { tag: "strong" }
64+
end
65+
66+
def test_assert_tag_ancestor
67+
# there is a 'li' tag with an ancestor having an id of 'foo'
68+
assert_tag ancestor: { attributes: { id: "foo" } }, tag: "li"
69+
# there is no tag of any kind with an ancestor having an href matching 'foo'
70+
assert_no_tag ancestor: { attributes: { href: /foo/ } }
71+
end
72+
73+
def test_assert_tag_descendant
74+
# there is a tag with a descendant 'li' tag
75+
assert_tag descendant: { tag: "li" }
76+
# there is no tag with a descendant 'html' tag
77+
assert_no_tag descendant: { tag: "html" }
78+
end
79+
80+
def test_assert_tag_sibling
81+
# there is a tag with a sibling of class 'item'
82+
assert_tag sibling: { attributes: { class: "item" } }
83+
# there is no tag with a sibling 'ul' tag
84+
assert_no_tag sibling: { tag: "ul" }
85+
end
86+
87+
def test_assert_tag_after
88+
# there is a tag following a sibling 'div' tag
89+
assert_tag after: { tag: "div" }
90+
# there is no tag following a sibling tag with id 'bar'
91+
assert_no_tag after: { attributes: { id: "bar" } }
92+
end
93+
94+
def test_assert_tag_before
95+
# there is a tag preceding a tag with id 'bar'
96+
assert_tag before: { attributes: { id: "bar" } }
97+
# there is no tag preceding a 'form' tag
98+
assert_no_tag before: { tag: "form" }
99+
end
100+
101+
def test_assert_tag_children_count
102+
# there is a tag with 2 children
103+
assert_tag children: { count: 2 }
104+
# in particular, there is a <ul> tag with two children (a nameless pair of <li>s)
105+
assert_tag tag: 'ul', children: { count: 2 }
106+
# there is no tag with 4 children
107+
assert_no_tag children: { count: 4 }
108+
end
109+
110+
def test_assert_tag_children_less_than
111+
# there is a tag with less than 5 children
112+
assert_tag children: { less_than: 5 }
113+
# there is no 'ul' tag with less than 2 children
114+
assert_no_tag children: { less_than: 2 }, tag: "ul"
115+
end
116+
117+
def test_assert_tag_children_greater_than
118+
# there is a 'body' tag with more than 1 children
119+
assert_tag children: { greater_than: 1 }, tag: "body"
120+
# there is no tag with more than 10 children
121+
assert_no_tag children: { greater_than: 10 }
122+
end
123+
124+
def test_assert_tag_children_only
125+
# there is a tag containing only one child with an id of 'foo'
126+
assert_tag children: { count: 1,
127+
only: { attributes: { id: "foo" } } }
128+
# there is no tag containing only one 'li' child
129+
assert_no_tag children: { count: 1, only: { tag: "li" } }
130+
end
131+
132+
def test_assert_tag_content
133+
# the output contains the string "Name"
134+
assert_tag content: /Name/
135+
# the output does not contain the string "test"
136+
assert_no_tag content: /test/
137+
end
138+
139+
def test_assert_tag_multiple
140+
# there is a 'div', id='bar', with an immediate child whose 'action'
141+
# attribute matches the regexp /somewhere/.
142+
assert_tag tag: "div", attributes: { id: "bar" },
143+
child: { attributes: { action: /somewhere/ } }
144+
145+
# there is no 'div', id='foo', with a 'ul' child with more than
146+
# 2 "li" children.
147+
assert_no_tag tag: "div", attributes: { id: "foo" },
148+
child: { tag: "ul",
149+
children: { greater_than: 2, only: { tag: "li" } } }
150+
end
151+
152+
def test_assert_tag_children_without_content
153+
# there is a form tag with an 'input' child which is a self closing tag
154+
assert_tag tag: "form",
155+
children: { count: 1,
156+
only: { tag: "input" } }
157+
158+
# the body tag has an 'a' child which in turn has an 'img' child
159+
assert_tag tag: "body",
160+
children: { count: 1,
161+
only: { tag: "a",
162+
children: { count: 1,
163+
only: { tag: "img" } } } }
164+
end
165+
166+
def test_assert_tag_attribute_matching
167+
@response.body = '<input type="text" name="my_name">'
168+
assert_tag tag: 'input',
169+
attributes: { name: /my/, type: 'text' }
170+
assert_no_tag tag: 'input',
171+
attributes: { name: 'my', type: 'text' }
172+
assert_no_tag tag: 'input',
173+
attributes: { name: /^my$/, type: 'text' }
174+
end
175+
176+
def test_assert_tag_content_matching
177+
@response.body = "<p>hello world</p>"
178+
assert_tag tag: "p", content: "hello world"
179+
assert_tag tag: "p", content: /hello/
180+
assert_no_tag tag: "p", content: "hello"
181+
end
182+
end

0 commit comments

Comments
 (0)