dommy 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +213 -0
- data/lib/dommy/attr.rb +200 -0
- data/lib/dommy/blob.rb +182 -0
- data/lib/dommy/bridge.rb +141 -0
- data/lib/dommy/css.rb +283 -0
- data/lib/dommy/custom_elements.rb +125 -0
- data/lib/dommy/data_transfer.rb +98 -0
- data/lib/dommy/document.rb +674 -0
- data/lib/dommy/dom_exception.rb +258 -0
- data/lib/dommy/dom_parser.rb +88 -0
- data/lib/dommy/element.rb +1975 -0
- data/lib/dommy/event.rb +589 -0
- data/lib/dommy/fetch.rb +241 -0
- data/lib/dommy/form_data.rb +208 -0
- data/lib/dommy/html_collection.rb +207 -0
- data/lib/dommy/html_elements.rb +4455 -0
- data/lib/dommy/internal/cookie_jar.rb +27 -0
- data/lib/dommy/internal/dom_matching.rb +141 -0
- data/lib/dommy/internal/mutation_coordinator.rb +172 -0
- data/lib/dommy/internal/node_traversal.rb +36 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
- data/lib/dommy/internal/observer_manager.rb +31 -0
- data/lib/dommy/internal/observer_matcher.rb +31 -0
- data/lib/dommy/internal/scope_resolution.rb +27 -0
- data/lib/dommy/internal/shadow_root_registry.rb +35 -0
- data/lib/dommy/internal/template_content_registry.rb +97 -0
- data/lib/dommy/minitest/assertions.rb +105 -0
- data/lib/dommy/minitest.rb +17 -0
- data/lib/dommy/navigator.rb +271 -0
- data/lib/dommy/node.rb +218 -0
- data/lib/dommy/observer.rb +199 -0
- data/lib/dommy/parser.rb +29 -0
- data/lib/dommy/promise.rb +199 -0
- data/lib/dommy/router.rb +275 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
- data/lib/dommy/rspec/matchers.rb +230 -0
- data/lib/dommy/rspec.rb +18 -0
- data/lib/dommy/scheduler.rb +135 -0
- data/lib/dommy/shadow_root.rb +255 -0
- data/lib/dommy/storage.rb +112 -0
- data/lib/dommy/test_helpers.rb +78 -0
- data/lib/dommy/tree_walker.rb +425 -0
- data/lib/dommy/url.rb +479 -0
- data/lib/dommy/version.rb +5 -0
- data/lib/dommy/world.rb +209 -0
- data/lib/dommy.rb +119 -0
- metadata +110 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internal/dom_matching"
|
|
4
|
+
require_relative "../internal/scope_resolution"
|
|
5
|
+
|
|
6
|
+
module Dommy
|
|
7
|
+
module RSpec
|
|
8
|
+
# Capybara-style RSpec matchers backed by Dommy.
|
|
9
|
+
#
|
|
10
|
+
# These mirror Capybara's matcher names (`have_selector`, `have_content`,
|
|
11
|
+
# `have_link`, etc.) so existing Capybara test suites can be migrated
|
|
12
|
+
# to a pure-Ruby DOM stack with minimal code changes.
|
|
13
|
+
#
|
|
14
|
+
# Because the names collide with Capybara::RSpecMatchers, this module
|
|
15
|
+
# should be included into specs that DO NOT also include Capybara —
|
|
16
|
+
# typically view / component / request specs, with feature specs
|
|
17
|
+
# left to Capybara. See the README for a `type:` split example.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# require "dommy/rspec/capy_style_matchers"
|
|
21
|
+
#
|
|
22
|
+
# RSpec.configure do |c|
|
|
23
|
+
# c.include Dommy::TestHelpers, type: :view
|
|
24
|
+
# c.include Dommy::RSpec::CapyStyleMatchers, type: :view
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# expect(dom).to have_selector("button.primary")
|
|
28
|
+
# expect(dom).to have_content("Welcome")
|
|
29
|
+
# expect(dom).to have_link("Sign up", href: "/signup")
|
|
30
|
+
module CapyStyleMatchers
|
|
31
|
+
def have_selector(selector, **opts)
|
|
32
|
+
HaveSelector.new(selector, **opts)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias_method :have_css, :have_selector
|
|
36
|
+
|
|
37
|
+
def have_no_selector(selector, **opts)
|
|
38
|
+
HaveNoSelector.new(selector, **opts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :have_no_css, :have_no_selector
|
|
42
|
+
|
|
43
|
+
def have_content(text, **opts)
|
|
44
|
+
HaveContent.new(text, **opts)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias_method :have_text, :have_content
|
|
48
|
+
|
|
49
|
+
def have_no_content(text, **opts)
|
|
50
|
+
HaveNoContent.new(text, **opts)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
alias_method :have_no_text, :have_no_content
|
|
54
|
+
|
|
55
|
+
def have_link(text = nil, **opts)
|
|
56
|
+
HaveLink.new(text, **opts)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def have_no_link(text = nil, **opts)
|
|
60
|
+
HaveNoLink.new(text, **opts)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def have_button(text = nil, **opts)
|
|
64
|
+
HaveButton.new(text, **opts)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def have_no_button(text = nil, **opts)
|
|
68
|
+
HaveNoButton.new(text, **opts)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def have_field(name_or_label = nil, **opts)
|
|
72
|
+
HaveField.new(name_or_label, **opts)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def have_no_field(name_or_label = nil, **opts)
|
|
76
|
+
HaveNoField.new(name_or_label, **opts)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ----- Base behavior shared across element-finding matchers -----
|
|
80
|
+
|
|
81
|
+
# @api private
|
|
82
|
+
class Base
|
|
83
|
+
def initialize(selector, **opts)
|
|
84
|
+
@selector = selector
|
|
85
|
+
@text = opts[:text] || opts[:content]
|
|
86
|
+
@count = opts[:count]
|
|
87
|
+
@exact = opts[:exact] || opts[:exact_text]
|
|
88
|
+
@visible = opts.fetch(:visible, :visible)
|
|
89
|
+
# :wait is accepted for Capybara compatibility but ignored
|
|
90
|
+
# (Dommy is synchronous).
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def matches?(scope)
|
|
94
|
+
@scope = Internal::ScopeResolution.resolve(scope)
|
|
95
|
+
@matched = find_matches(@scope)
|
|
96
|
+
count_ok?(@matched.size)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def does_not_match?(scope)
|
|
100
|
+
@scope = Internal::ScopeResolution.resolve(scope)
|
|
101
|
+
@matched = find_matches(@scope)
|
|
102
|
+
if @count
|
|
103
|
+
!count_ok?(@matched.size)
|
|
104
|
+
else
|
|
105
|
+
@matched.empty?
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def description
|
|
110
|
+
"have #{describe_target}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def failure_message
|
|
114
|
+
"expected to find #{describe_target}, found #{@matched.size}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def failure_message_when_negated
|
|
118
|
+
"expected NOT to find #{describe_target}, found #{@matched.size}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def find_matches(scope)
|
|
124
|
+
elements = scope.query_selector_all(query_selector).to_a
|
|
125
|
+
elements = filter_by_text(elements) if @text
|
|
126
|
+
Internal::DomMatching.filter_by_visibility(elements, @visible)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def filter_by_text(elements)
|
|
130
|
+
elements.select { |el| Internal::DomMatching.text_matches?(el.text_content, @text, exact: @exact) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def query_selector
|
|
134
|
+
@selector
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def count_ok?(actual)
|
|
138
|
+
Internal::DomMatching.count_matches?(actual, @count)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def describe_target
|
|
142
|
+
parts = [describe_what]
|
|
143
|
+
parts << "with text #{@text.inspect}" if @text
|
|
144
|
+
parts << "(count: #{@count.inspect})" if @count
|
|
145
|
+
parts.join(" ")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def describe_what
|
|
149
|
+
"elements matching #{@selector.inspect}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @api private
|
|
154
|
+
# Mixin that flips matches? / does_not_match? for the "no_*" matchers,
|
|
155
|
+
# so each negative matcher reads identically to its positive twin
|
|
156
|
+
# but with inverted assertions.
|
|
157
|
+
module Negated
|
|
158
|
+
def matches?(scope)
|
|
159
|
+
!super
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def does_not_match?(scope)
|
|
163
|
+
!matches?(scope)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def failure_message
|
|
167
|
+
"expected NOT to find #{describe_target}, found #{@matched.size}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def failure_message_when_negated
|
|
171
|
+
"expected to find #{describe_target}, found #{@matched.size}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class HaveSelector < Base
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class HaveNoSelector < HaveSelector
|
|
179
|
+
include Negated
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @api private
|
|
183
|
+
class HaveContent
|
|
184
|
+
def initialize(text, **opts)
|
|
185
|
+
@text = text
|
|
186
|
+
@exact = opts[:exact] || opts[:exact_text]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def matches?(scope)
|
|
190
|
+
@scope = Internal::ScopeResolution.resolve(scope)
|
|
191
|
+
@actual = Internal::DomMatching.text_of(@scope)
|
|
192
|
+
Internal::DomMatching.text_matches?(@actual, @text, exact: @exact)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def does_not_match?(scope)
|
|
196
|
+
!matches?(scope)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def description
|
|
200
|
+
"have content #{@text.inspect}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def failure_message
|
|
204
|
+
"expected text to include #{@text.inspect}, got #{@actual.inspect}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def failure_message_when_negated
|
|
208
|
+
"expected text NOT to include #{@text.inspect}, got #{@actual.inspect}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @api private
|
|
213
|
+
class HaveNoContent < HaveContent
|
|
214
|
+
def matches?(scope)
|
|
215
|
+
!super
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def does_not_match?(scope)
|
|
219
|
+
!matches?(scope)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def failure_message
|
|
223
|
+
"expected text NOT to include #{@text.inspect}, got #{@actual.inspect}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def failure_message_when_negated
|
|
227
|
+
"expected text to include #{@text.inspect}, got #{@actual.inspect}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# @api private
|
|
232
|
+
class HaveLink < Base
|
|
233
|
+
def initialize(text, **opts)
|
|
234
|
+
super("a[href]", text: text, **opts.reject { |k, _| k == :href })
|
|
235
|
+
@href = opts[:href]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def find_matches(scope)
|
|
241
|
+
elements = super
|
|
242
|
+
return elements unless @href
|
|
243
|
+
|
|
244
|
+
elements.select do |a|
|
|
245
|
+
href = a.get_attribute("href").to_s
|
|
246
|
+
case @href
|
|
247
|
+
when Regexp
|
|
248
|
+
href.match?(@href)
|
|
249
|
+
else
|
|
250
|
+
href == @href.to_s
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def describe_what
|
|
256
|
+
@href ? "link to #{@href.inspect}" : "link"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
class HaveNoLink < HaveLink
|
|
261
|
+
include Negated
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @api private
|
|
265
|
+
class HaveButton < Base
|
|
266
|
+
def initialize(text, **opts)
|
|
267
|
+
# Buttons are <button> elements OR <input type="submit|button|reset">.
|
|
268
|
+
super("button, input[type='submit'], input[type='button'], input[type='reset']", text: text, **opts)
|
|
269
|
+
@type_filter = opts[:type]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def find_matches(scope)
|
|
275
|
+
elements = super
|
|
276
|
+
return elements unless @type_filter
|
|
277
|
+
|
|
278
|
+
elements.select { |el| el.get_attribute("type") == @type_filter.to_s }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Override Base's text filter so <input type=submit> matches by
|
|
282
|
+
# its `value` attribute rather than text content.
|
|
283
|
+
def filter_by_text(elements)
|
|
284
|
+
elements.select do |el|
|
|
285
|
+
label = el.tag_name.downcase == "input" ? el.get_attribute("value").to_s : el.text_content.to_s
|
|
286
|
+
Internal::DomMatching.text_matches?(label, @text, exact: @exact)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def describe_what
|
|
291
|
+
"button"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
class HaveNoButton < HaveButton
|
|
296
|
+
include Negated
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# @api private
|
|
300
|
+
# have_field locates form fields by name attribute, id, or label text.
|
|
301
|
+
class HaveField < Base
|
|
302
|
+
def initialize(name_or_label, **opts)
|
|
303
|
+
super("input, textarea, select", text: nil, **opts.reject { |k, _| %i[with type].include?(k) })
|
|
304
|
+
@locator = name_or_label
|
|
305
|
+
@with_value = opts[:with]
|
|
306
|
+
@type_filter = opts[:type]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
private
|
|
310
|
+
|
|
311
|
+
def find_matches(scope)
|
|
312
|
+
elements = scope.query_selector_all(query_selector).to_a
|
|
313
|
+
elements = elements.select { |el| matches_locator?(el, @locator) } if @locator
|
|
314
|
+
elements = elements.select { |el| el.get_attribute("type") == @type_filter.to_s } if @type_filter
|
|
315
|
+
elements = elements.select { |el| matches_value?(el, @with_value) } unless @with_value.nil?
|
|
316
|
+
Internal::DomMatching.filter_by_visibility(elements, @visible)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def matches_locator?(el, locator)
|
|
320
|
+
locator_str = locator.to_s
|
|
321
|
+
return true if el.get_attribute("name") == locator_str
|
|
322
|
+
return true if el.get_attribute("id") == locator_str
|
|
323
|
+
return true if matches_label?(el, locator_str)
|
|
324
|
+
|
|
325
|
+
false
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Find a <label> with matching text whose `for=` points to this element,
|
|
329
|
+
# or that wraps this element.
|
|
330
|
+
def matches_label?(el, label_text)
|
|
331
|
+
@scope.query_selector_all("label").any? do |label|
|
|
332
|
+
next false unless label.text_content.to_s.strip.include?(label_text)
|
|
333
|
+
|
|
334
|
+
for_attr = label.get_attribute("for")
|
|
335
|
+
next true if for_attr && el.get_attribute("id") == for_attr
|
|
336
|
+
|
|
337
|
+
label.contains?(el)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def matches_value?(el, expected_value)
|
|
342
|
+
actual = el.respond_to?(:value) ? el.value.to_s : el.get_attribute("value").to_s
|
|
343
|
+
actual == expected_value.to_s
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def describe_what
|
|
347
|
+
@locator ? "field #{@locator.inspect}" : "field"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
class HaveNoField < HaveField
|
|
352
|
+
include Negated
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internal/dom_matching"
|
|
4
|
+
require_relative "../internal/scope_resolution"
|
|
5
|
+
|
|
6
|
+
module Dommy
|
|
7
|
+
module RSpec
|
|
8
|
+
# Custom RSpec matchers for asserting against Dommy DOM objects.
|
|
9
|
+
#
|
|
10
|
+
# @example RSpec config
|
|
11
|
+
# require "dommy/rspec"
|
|
12
|
+
# RSpec.configure do |c|
|
|
13
|
+
# c.include Dommy::RSpec::Matchers
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @example Usage
|
|
17
|
+
# expect(root).to contain_dom("button.primary")
|
|
18
|
+
# expect(root).to contain_dom("li", count: 3)
|
|
19
|
+
# expect(root).to contain_dom("h1", text: "Welcome")
|
|
20
|
+
# expect(root).to contain_dom_text("Submit")
|
|
21
|
+
# expect(button).to have_dom_attribute("type", "button")
|
|
22
|
+
# expect(button).to have_dom_class("primary")
|
|
23
|
+
# expect(root).to match_dom_html("<h1>Hello</h1>")
|
|
24
|
+
module Matchers
|
|
25
|
+
def contain_dom(selector, text: nil, count: nil)
|
|
26
|
+
ContainDom.new(selector, text: text, count: count)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def contain_dom_text(text)
|
|
30
|
+
ContainDomText.new(text)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def have_dom_attribute(name, value = UNSET)
|
|
34
|
+
HaveDomAttribute.new(name, value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def have_dom_class(class_name)
|
|
38
|
+
HaveDomClass.new(class_name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def match_dom_html(expected_html)
|
|
42
|
+
MatchDomHtml.new(expected_html)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Sentinel for "value was not passed" — distinguishes
|
|
46
|
+
# `have_dom_attribute("disabled")` (existence only) from
|
|
47
|
+
# `have_dom_attribute("disabled", "true")` (value match).
|
|
48
|
+
UNSET = Object.new.freeze
|
|
49
|
+
private_constant :UNSET
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
class ContainDom
|
|
53
|
+
def initialize(selector, text: nil, count: nil)
|
|
54
|
+
@selector = selector
|
|
55
|
+
@text = text
|
|
56
|
+
@count = count
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def matches?(scope)
|
|
60
|
+
@scope = Internal::ScopeResolution.resolve(scope)
|
|
61
|
+
@matched = Internal::DomMatching.filter(@scope, @selector, text: @text)
|
|
62
|
+
Internal::DomMatching.count_matches?(@matched.size, @count)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def does_not_match?(scope)
|
|
66
|
+
!matches?(scope)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def description
|
|
70
|
+
parts = ["contain DOM matching #{@selector.inspect}"]
|
|
71
|
+
parts << "with text #{@text.inspect}" if @text
|
|
72
|
+
parts << "(count: #{@count.inspect})" if @count
|
|
73
|
+
parts.join(" ")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def failure_message
|
|
77
|
+
"expected #{describe_scope} to #{description} (found #{@matched.size})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def failure_message_when_negated
|
|
81
|
+
"expected #{describe_scope} not to #{description} (found #{@matched.size})"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def describe_scope
|
|
87
|
+
@scope.respond_to?(:tag_name) ? "<#{@scope.tag_name.downcase}>" : @scope.class.name
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @api private
|
|
92
|
+
class ContainDomText
|
|
93
|
+
def initialize(text)
|
|
94
|
+
@text = text
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def matches?(scope)
|
|
98
|
+
@scope = Internal::ScopeResolution.resolve(scope)
|
|
99
|
+
@actual_text = Internal::DomMatching.text_of(@scope)
|
|
100
|
+
Internal::DomMatching.text_matches?(@actual_text, @text)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def does_not_match?(scope)
|
|
104
|
+
!matches?(scope)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def description
|
|
108
|
+
"contain text #{@text.inspect}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def failure_message
|
|
112
|
+
"expected text to include #{@text.inspect}, got #{@actual_text.inspect}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def failure_message_when_negated
|
|
116
|
+
"expected text not to include #{@text.inspect}, got #{@actual_text.inspect}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @api private
|
|
121
|
+
class HaveDomAttribute
|
|
122
|
+
def initialize(name, value)
|
|
123
|
+
@name = name.to_s
|
|
124
|
+
@value = value
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def matches?(element)
|
|
128
|
+
@element = element
|
|
129
|
+
return false unless element.has_attribute?(@name)
|
|
130
|
+
|
|
131
|
+
@actual = element.get_attribute(@name)
|
|
132
|
+
unset?(@value) ? true : @actual.to_s == @value.to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def does_not_match?(element)
|
|
136
|
+
!matches?(element)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def description
|
|
140
|
+
if unset?(@value)
|
|
141
|
+
"have DOM attribute #{@name.inspect}"
|
|
142
|
+
else
|
|
143
|
+
"have DOM attribute #{@name.inspect} = #{@value.inspect}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def failure_message
|
|
148
|
+
if unset?(@value)
|
|
149
|
+
"expected element to have attribute #{@name.inspect}, but it was missing"
|
|
150
|
+
else
|
|
151
|
+
"expected attribute #{@name.inspect} to equal #{@value.inspect}, got #{@actual.inspect}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def failure_message_when_negated
|
|
156
|
+
if unset?(@value)
|
|
157
|
+
"expected element NOT to have attribute #{@name.inspect}, but it was present (#{@actual.inspect})"
|
|
158
|
+
else
|
|
159
|
+
"expected attribute #{@name.inspect} not to equal #{@value.inspect}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def unset?(value)
|
|
166
|
+
value.equal?(UNSET)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @api private
|
|
171
|
+
class HaveDomClass
|
|
172
|
+
def initialize(class_name)
|
|
173
|
+
@class_name = class_name.to_s
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def matches?(element)
|
|
177
|
+
@element = element
|
|
178
|
+
@actual_classes = element.class_list.value.to_s.split(/\s+/)
|
|
179
|
+
@actual_classes.include?(@class_name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def does_not_match?(element)
|
|
183
|
+
!matches?(element)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def description
|
|
187
|
+
"have DOM class #{@class_name.inspect}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def failure_message
|
|
191
|
+
"expected element to have class #{@class_name.inspect}, got #{@actual_classes.inspect}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def failure_message_when_negated
|
|
195
|
+
"expected element NOT to have class #{@class_name.inspect}, got #{@actual_classes.inspect}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# @api private
|
|
200
|
+
class MatchDomHtml
|
|
201
|
+
def initialize(expected_html)
|
|
202
|
+
@expected_html = expected_html
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def matches?(scope)
|
|
206
|
+
@scope = Internal::ScopeResolution.resolve(scope)
|
|
207
|
+
@actual_normalized = Internal::DomMatching.normalize_html(Internal::DomMatching.html_of(@scope))
|
|
208
|
+
@expected_normalized = Internal::DomMatching.normalize_html(@expected_html)
|
|
209
|
+
@actual_normalized == @expected_normalized
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def does_not_match?(scope)
|
|
213
|
+
!matches?(scope)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def description
|
|
217
|
+
"match DOM HTML #{@expected_html.inspect}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def failure_message
|
|
221
|
+
"expected DOM HTML to match.\nExpected: #{@expected_normalized}\nActual: #{@actual_normalized}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def failure_message_when_negated
|
|
225
|
+
"expected DOM HTML NOT to match #{@expected_normalized}"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
data/lib/dommy/rspec.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Entry point for using Dommy from RSpec test suites.
|
|
4
|
+
# Loads the test helpers and DOM matcher modules so users can
|
|
5
|
+
# `include` them into their RSpec config.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# require "dommy/rspec"
|
|
9
|
+
#
|
|
10
|
+
# RSpec.configure do |c|
|
|
11
|
+
# c.include Dommy::TestHelpers
|
|
12
|
+
# c.include Dommy::RSpec::Matchers
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
require "dommy"
|
|
16
|
+
require "dommy/test_helpers"
|
|
17
|
+
require "dommy/rspec/matchers"
|
|
18
|
+
require "dommy/rspec/capy_style_matchers"
|