dommy-rails 0.9.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.
@@ -0,0 +1,298 @@
1
+ module Dommy
2
+ module Rails
3
+ module Minitest
4
+ module Assertions
5
+ def assert_dom_has_css(scope, selector, text: nil, count: nil, msg: nil)
6
+ assert_dom_contains(scope, selector, text: text, count: count, msg: msg)
7
+ end
8
+
9
+ def refute_dom_has_css(scope, selector, text: nil, count: nil, msg: nil)
10
+ refute_dom_contains(scope, selector, text: text, count: count, msg: msg)
11
+ end
12
+
13
+ def assert_dom_has_text(scope, text, msg: nil)
14
+ assert_dom_contains_text(scope, text, msg: msg)
15
+ end
16
+
17
+ def refute_dom_has_text(scope, text, msg: nil)
18
+ refute_dom_contains_text(scope, text, msg: msg)
19
+ end
20
+
21
+ def assert_dom_has_xpath(actual, expression, text: nil, count: nil, msg: nil)
22
+ matched = Dommy::Rails::PageInspector.xpath_matches(dom_document_for(actual), expression, text: text)
23
+ msg ||= "expected to contain XPath #{expression.inspect}#{text ? " with text #{text.inspect}" : ""}, found #{matched.size}"
24
+ assert(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
25
+ end
26
+
27
+ def refute_dom_has_xpath(actual, expression, text: nil, count: nil, msg: nil)
28
+ matched = Dommy::Rails::PageInspector.xpath_matches(dom_document_for(actual), expression, text: text)
29
+ msg ||= "expected NOT to contain XPath #{expression.inspect}#{text ? " with text #{text.inspect}" : ""}, found #{matched.size}"
30
+ refute(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
31
+ end
32
+
33
+ def assert_dom_has_title(actual, expected, msg: nil)
34
+ matched = Dommy::Rails::PageInspector.title_matches?(dom_document_for(actual), expected)
35
+ msg ||= "expected document title to match #{expected.inspect}"
36
+ assert(matched, msg)
37
+ end
38
+
39
+ def refute_dom_has_title(actual, expected, msg: nil)
40
+ matched = Dommy::Rails::PageInspector.title_matches?(dom_document_for(actual), expected)
41
+ msg ||= "expected document title NOT to match #{expected.inspect}"
42
+ refute(matched, msg)
43
+ end
44
+
45
+ # Assert the subject's ARIA snapshot matches `expected` (Playwright-style
46
+ # subset; names may be /regex/).
47
+ def assert_aria_snapshot(expected, actual, msg: nil)
48
+ snapshot = dom_document_for(actual).aria_snapshot
49
+ msg ||= "expected aria snapshot to match:\n#{expected}\ngot:\n#{snapshot}"
50
+ assert(Dommy::Rails::AriaSnapshotMatching.matches?(snapshot, expected), msg)
51
+ end
52
+
53
+ def assert_dom_has_meta(actual, name: nil, property: nil, content: nil, msg: nil)
54
+ matched = Dommy::Rails::PageInspector.meta_matches?(dom_document_for(actual), name: name, property: property, content: content)
55
+ msg ||= "expected to find meta #{meta_desc(name: name, property: property, content: content)}"
56
+ assert(matched, msg)
57
+ end
58
+
59
+ def refute_dom_has_meta(actual, name: nil, property: nil, content: nil, msg: nil)
60
+ matched = Dommy::Rails::PageInspector.meta_matches?(dom_document_for(actual), name: name, property: property, content: content)
61
+ msg ||= "expected NOT to find meta #{meta_desc(name: name, property: property, content: content)}"
62
+ refute(matched, msg)
63
+ end
64
+
65
+ def assert_dom_has_csrf_meta_tags(actual, msg: nil)
66
+ matched = Dommy::Rails::PageInspector.csrf_meta_tags?(dom_document_for(actual))
67
+ msg ||= "expected to find Rails CSRF meta tags"
68
+ assert(matched, msg)
69
+ end
70
+
71
+ def assert_dom_has_authenticity_token(actual, msg: nil)
72
+ matched = Dommy::Rails::PageInspector.authenticity_token?(dom_document_for(actual))
73
+ msg ||= "expected to find Rails authenticity token field"
74
+ assert(matched, msg)
75
+ end
76
+
77
+ def assert_dom_has_link(actual, text = nil, href: nil, count: nil, msg: nil)
78
+ matched = Dommy::Rails::PageInspector.links(dom_document_for(actual), text: text, href: href)
79
+ msg ||= "expected to contain link#{text ? " with text #{text.inspect}" : ""}#{href ? " with href #{href.inspect}" : ""}, found #{matched.size}"
80
+ assert(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
81
+ end
82
+
83
+ def refute_dom_has_link(actual, text = nil, href: nil, count: nil, msg: nil)
84
+ matched = Dommy::Rails::PageInspector.links(dom_document_for(actual), text: text, href: href)
85
+ msg ||= "expected NOT to contain link#{text ? " with text #{text.inspect}" : ""}#{href ? " with href #{href.inspect}" : ""}, found #{matched.size}"
86
+ refute(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
87
+ end
88
+
89
+ def assert_dom_has_turbo_frame(actual, id = nil, text: nil, count: nil, msg: nil)
90
+ matched = Dommy::Rails::PageInspector.turbo_frames(dom_document_for(actual), id, text: text)
91
+ msg ||= "expected to contain turbo-frame#{id ? " ##{id}" : ""}#{text ? " with text #{text.inspect}" : ""}, found #{matched.size}"
92
+ assert(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
93
+ yield matched.first if block_given? && matched.any?
94
+ end
95
+
96
+ def refute_dom_has_turbo_frame(actual, id = nil, text: nil, count: nil, msg: nil)
97
+ matched = Dommy::Rails::PageInspector.turbo_frames(dom_document_for(actual), id, text: text)
98
+ msg ||= "expected NOT to contain turbo-frame#{id ? " ##{id}" : ""}#{text ? " with text #{text.inspect}" : ""}, found #{matched.size}"
99
+ refute(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
100
+ end
101
+
102
+ def assert_dom_has_select(actual, name = nil, label: nil, count: nil, msg: nil)
103
+ matched = Dommy::Rails::PageInspector.selects(dom_document_for(actual), name: name, label: label)
104
+ msg ||= "expected to contain select#{name ? " with name #{name.inspect}" : ""}#{label ? " with label #{label.inspect}" : ""}, found #{matched.size}"
105
+ assert(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
106
+ end
107
+
108
+ def refute_dom_has_select(actual, name = nil, label: nil, count: nil, msg: nil)
109
+ matched = Dommy::Rails::PageInspector.selects(dom_document_for(actual), name: name, label: label)
110
+ msg ||= "expected NOT to contain select#{name ? " with name #{name.inspect}" : ""}#{label ? " with label #{label.inspect}" : ""}, found #{matched.size}"
111
+ refute(Dommy::Internal::DomMatching.count_matches?(matched.size, count), msg)
112
+ end
113
+
114
+ def assert_dom_has_checked_field(actual, name = nil, msg: nil)
115
+ matched = Dommy::Rails::PageInspector.checkable_fields(dom_document_for(actual), name: name, checked: true)
116
+ msg ||= "expected to find checked field#{name ? " #{name.inspect}" : ""}"
117
+ assert(matched.any?, msg)
118
+ end
119
+
120
+ def assert_dom_has_unchecked_field(actual, name = nil, msg: nil)
121
+ matched = Dommy::Rails::PageInspector.checkable_fields(dom_document_for(actual), name: name, checked: false)
122
+ msg ||= "expected to find unchecked field#{name ? " #{name.inspect}" : ""}"
123
+ assert(matched.any?, msg)
124
+ end
125
+
126
+ def assert_dom_has_form(actual, action: nil, method: nil, model: nil, msg: nil)
127
+ document = dom_document_for(actual)
128
+ matched = Dommy::Rails::FormInspector.matches?(document, action: action, method: method, model: model)
129
+ msg ||= "expected to find form matching #{form_desc(action: action, method: method, model: model)}"
130
+ assert(matched, msg)
131
+ end
132
+
133
+ def refute_dom_has_form(actual, action: nil, method: nil, model: nil, msg: nil)
134
+ document = dom_document_for(actual)
135
+ matched = Dommy::Rails::FormInspector.matches?(document, action: action, method: method, model: model)
136
+ msg ||= "expected NOT to find form matching #{form_desc(action: action, method: method, model: model)}"
137
+ refute(matched, msg)
138
+ end
139
+
140
+ def assert_dom_has_turbo_stream(actual, action:, target:, msg: nil)
141
+ stream = Dommy::Rails::TurboStream.find(dom_body_for(actual), action: action, target: target)
142
+ msg ||= "expected to find turbo-stream action=#{action} target=#{target}"
143
+ assert(stream, msg)
144
+ yield Dommy::Rails::TurboStream.fragment_document(stream) if block_given? && stream
145
+ end
146
+
147
+ def refute_dom_has_turbo_stream(actual, action:, target:, msg: nil)
148
+ matched = Dommy::Rails::TurboStream.matches?(dom_body_for(actual), action: action, target: target)
149
+ msg ||= "expected NOT to find turbo-stream action=#{action} target=#{target}"
150
+ refute(matched, msg)
151
+ end
152
+
153
+ def assert_dom_appends_turbo_stream(actual, target, msg: nil, &block)
154
+ assert_dom_has_turbo_stream(actual, action: "append", target: target, msg: msg, &block)
155
+ end
156
+
157
+ def assert_dom_replaces_turbo_stream(actual, target, msg: nil, &block)
158
+ assert_dom_has_turbo_stream(actual, action: "replace", target: target, msg: msg, &block)
159
+ end
160
+
161
+ def assert_dom_updates_turbo_stream(actual, target, msg: nil, &block)
162
+ assert_dom_has_turbo_stream(actual, action: "update", target: target, msg: msg, &block)
163
+ end
164
+
165
+ def assert_dom_removes_turbo_stream(actual, target, msg: nil, &block)
166
+ assert_dom_has_turbo_stream(actual, action: "remove", target: target, msg: msg, &block)
167
+ end
168
+
169
+ def assert_dom_has_stimulus_controller(actual, name, msg: nil)
170
+ matched = Dommy::Rails::Stimulus.controller?(dom_document_for(actual), name)
171
+ msg ||= "expected to find element with Stimulus controller '#{name}'"
172
+ assert(matched, msg)
173
+ end
174
+
175
+ def refute_dom_has_stimulus_controller(actual, name, msg: nil)
176
+ matched = Dommy::Rails::Stimulus.controller?(dom_document_for(actual), name)
177
+ msg ||= "expected NOT to find element with Stimulus controller '#{name}'"
178
+ refute(matched, msg)
179
+ end
180
+
181
+ def assert_dom_has_stimulus_action(actual, action, msg: nil)
182
+ matched = Dommy::Rails::Stimulus.action?(dom_document_for(actual), action)
183
+ msg ||= "expected to find element with Stimulus action '#{action}'"
184
+ assert(matched, msg)
185
+ end
186
+
187
+ def refute_dom_has_stimulus_action(actual, action, msg: nil)
188
+ matched = Dommy::Rails::Stimulus.action?(dom_document_for(actual), action)
189
+ msg ||= "expected NOT to find element with Stimulus action '#{action}'"
190
+ refute(matched, msg)
191
+ end
192
+
193
+ def assert_dom_has_stimulus_target(actual, controller, target, msg: nil)
194
+ matched = Dommy::Rails::Stimulus.target?(dom_document_for(actual), controller, target)
195
+ msg ||= "expected to find element with Stimulus target '#{controller}.#{target}'"
196
+ assert(matched, msg)
197
+ end
198
+
199
+ def refute_dom_has_stimulus_target(actual, controller, target, msg: nil)
200
+ matched = Dommy::Rails::Stimulus.target?(dom_document_for(actual), controller, target)
201
+ msg ||= "expected NOT to find element with Stimulus target '#{controller}.#{target}'"
202
+ refute(matched, msg)
203
+ end
204
+
205
+ def assert_dom_has_stimulus_value(actual, controller, key, value, msg: nil)
206
+ matched = Dommy::Rails::Stimulus.value?(dom_document_for(actual), controller, key, value)
207
+ msg ||= "expected to find element with Stimulus value '#{controller}.#{key}' = #{value.inspect}"
208
+ assert(matched, msg)
209
+ end
210
+
211
+ def refute_dom_has_stimulus_value(actual, controller, key, value, msg: nil)
212
+ matched = Dommy::Rails::Stimulus.value?(dom_document_for(actual), controller, key, value)
213
+ msg ||= "expected NOT to find element with Stimulus value '#{controller}.#{key}' = #{value.inspect}"
214
+ refute(matched, msg)
215
+ end
216
+
217
+ def assert_dom_no_duplicate_ids(actual, msg: nil)
218
+ document = dom_document_for(actual)
219
+ duplicates = Dommy::Rails::Lint.duplicate_ids(document)
220
+ msg ||= "expected no duplicate IDs, found: #{duplicates.join(', ')}"
221
+ assert(duplicates.empty?, msg)
222
+ end
223
+
224
+ def assert_dom_no_invalid_aria_references(actual, msg: nil)
225
+ document = dom_document_for(actual)
226
+ issues = Dommy::Rails::Lint.invalid_aria_references(document)
227
+ msg ||= "expected no invalid ARIA references, found #{issues.size} issues"
228
+ assert(issues.empty?, msg)
229
+ end
230
+
231
+ def assert_dom_no_missing_form_labels(actual, msg: nil)
232
+ document = dom_document_for(actual)
233
+ issues = Dommy::Rails::Lint.missing_form_labels(document)
234
+ msg ||= "expected no missing form labels, found #{issues.size} issues"
235
+ assert(issues.empty?, msg)
236
+ end
237
+
238
+ def assert_dom_no_empty_links(actual, msg: nil)
239
+ issues = Dommy::Rails::Lint.empty_links(dom_document_for(actual))
240
+ msg ||= "expected no empty links, found #{issues.size} issues"
241
+ assert(issues.empty?, msg)
242
+ end
243
+
244
+ def assert_dom_no_nested_interactive_elements(actual, msg: nil)
245
+ issues = Dommy::Rails::Lint.nested_interactive_elements(dom_document_for(actual))
246
+ msg ||= "expected no nested interactive elements, found #{issues.size} issues"
247
+ assert(issues.empty?, msg)
248
+ end
249
+
250
+ def assert_mail_has_html_link(mail, text = nil, href: nil, count: nil, msg: nil)
251
+ document = Dommy::Rails::MailPart.html_document(mail)
252
+ msg ||= "expected mail to have an HTML part"
253
+ assert(document, msg)
254
+ assert_dom_has_link(document, text, href: href, count: count, msg: msg)
255
+ end
256
+
257
+ def assert_mail_has_html_text(mail, text, msg: nil)
258
+ document = Dommy::Rails::MailPart.html_document(mail)
259
+ msg ||= "expected mail HTML to contain #{text.inspect}"
260
+ assert(document, "expected mail to have an HTML part")
261
+ assert_dom_has_text(document, text, msg: msg)
262
+ end
263
+
264
+ def assert_mail_has_plain_text(mail, text, msg: nil)
265
+ body = Dommy::Rails::MailPart.plain_body(mail).to_s
266
+ msg ||= "expected mail plain text to contain #{text.inspect}, got #{body.inspect}"
267
+ assert(Dommy::Internal::DomMatching.text_matches?(body, text), msg)
268
+ end
269
+
270
+ private
271
+
272
+ def dom_document_for(actual)
273
+ Dommy::Rails::MatchTarget.document(actual)
274
+ end
275
+
276
+ def dom_body_for(actual)
277
+ Dommy::Rails::MatchTarget.body(actual)
278
+ end
279
+
280
+ def form_desc(action:, method:, model:)
281
+ parts = []
282
+ parts << "action=#{action}" if action
283
+ parts << "method=#{method}" if method
284
+ parts << "model=#{model}" if model
285
+ parts.empty? ? "any form" : parts.join(" ")
286
+ end
287
+
288
+ def meta_desc(name:, property:, content:)
289
+ parts = []
290
+ parts << "name=#{name}" if name
291
+ parts << "property=#{property}" if property
292
+ parts << "content=#{content}" if content
293
+ parts.empty? ? "matching any criteria" : parts.join(" ")
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Rails
5
+ module Minitest
6
+ module Integration
7
+ include Dommy::Rails::DomSource
8
+ include Dommy::Minitest::Assertions
9
+ include Dommy::Rails::Minitest::Assertions
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ require "dommy/minitest"
2
+ require_relative "../rails"
3
+ require_relative "minitest/assertions"
4
+ require_relative "minitest/integration"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dommy/internal/dom_matching"
4
+ require "dommy/internal/element_matching"
5
+ require_relative "url_matcher"
6
+
7
+ module Dommy
8
+ module Rails
9
+ module PageInspector
10
+ module_function
11
+
12
+ def title_matches?(document, expected)
13
+ Dommy::Internal::DomMatching.text_matches?(document.title.to_s, expected, exact: true)
14
+ end
15
+
16
+ def meta_matches?(document, name: nil, property: nil, content: nil)
17
+ document.query_selector_all("meta").to_a.any? do |meta|
18
+ (name.nil? || meta.get_attribute("name").to_s == name.to_s) &&
19
+ (property.nil? || meta.get_attribute("property").to_s == property.to_s) &&
20
+ (content.nil? || meta.get_attribute("content").to_s == content.to_s)
21
+ end
22
+ end
23
+
24
+ def csrf_meta_tags?(document)
25
+ param = document.query_selector("meta[name='csrf-param']")
26
+ token = document.query_selector("meta[name='csrf-token']")
27
+ present_content?(param) && present_content?(token)
28
+ end
29
+
30
+ def authenticity_token?(document)
31
+ document.query_selector_all("input[type='hidden'][name='authenticity_token']").to_a.any? do |input|
32
+ input.get_attribute("value").to_s != ""
33
+ end
34
+ end
35
+
36
+ def links(document, text: nil, href: nil)
37
+ Dommy::Internal::ElementMatching.find_links(document, text: text, href: href && UrlMatcher.new(href))
38
+ end
39
+
40
+ def turbo_frames(document, id = nil, text: nil)
41
+ frames = document.query_selector_all("turbo-frame").to_a
42
+ frames = frames.select { |frame| frame.get_attribute("id").to_s == id.to_s } if id
43
+ frames = frames.select { |frame| Dommy::Internal::DomMatching.text_matches?(frame.text_content, text) } if text
44
+ frames
45
+ end
46
+
47
+ def xpath_matches(document, expression, text: nil)
48
+ nodes = document.xpath(expression).to_a
49
+ nodes = nodes.select { |node| Dommy::Internal::DomMatching.text_matches?(node.text_content, text) } if text
50
+ nodes
51
+ end
52
+
53
+ def checkable_fields(document, name: nil, checked: nil)
54
+ Dommy::Internal::ElementMatching.find_checkable_fields(document, name: name, checked: checked)
55
+ end
56
+
57
+ def selects(document, name: nil, label: nil)
58
+ Dommy::Internal::ElementMatching.find_selects(document, name: name, label: label)
59
+ end
60
+
61
+ def present_content?(element)
62
+ element && element.get_attribute("content").to_s != ""
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../browser_spec"
4
+ require "dommy/rspec/capy_style_matchers"
5
+
6
+ # Opt-in RSpec wiring for browser specs: `require "dommy/rails/rspec/browser"`
7
+ # (e.g. in rails_helper), then tag example groups with `type: :browser` to get
8
+ # the `browser` helper (a javascript: true session over the Rails app) without
9
+ # an explicit `include`. Route URL helpers (`root_path`, …) are included too, so
10
+ # browser specs read like request specs.
11
+ #
12
+ # RSpec.describe "Todos", type: :browser do
13
+ # it "toggles a todo" do
14
+ # browser.visit root_path
15
+ # browser.click "li.todo"
16
+ # expect(browser).to have_css("li.todo.is-completed")
17
+ # end
18
+ # end
19
+ if defined?(::RSpec)
20
+ ::RSpec.configure do |config|
21
+ config.include Dommy::Rails::BrowserSpec, type: :browser
22
+
23
+ if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
24
+ config.include ::Rails.application.routes.url_helpers, type: :browser
25
+ end
26
+ end
27
+
28
+ # When a matcher fails on a trace-enabled session subject (a browser spec's
29
+ # `expect(browser).to have_text …`), append the current page and a recent
30
+ # trace to the failure message. Subjects without a trace (request/view specs'
31
+ # `expect(dom)`) are left untouched.
32
+ Dommy::RSpec.failure_context = lambda do |subject|
33
+ next nil unless subject.respond_to?(:trace) && subject.trace
34
+
35
+ trace = subject.trace
36
+ page = trace.current_page
37
+ "Current page: #{page[:url]} title=#{page[:title].inspect}\n\n" \
38
+ "Recent trace:\n#{trace.to_text(limit: 15)}"
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Rails
5
+ module RSpec
6
+ module Integration
7
+ include Dommy::Rails::DomSource
8
+ # Include order matters: Rails::RSpec::Matchers comes last so its
9
+ # Rails-specific `have_link` (URL-normalizing `href:` matching)
10
+ # deliberately overrides the CapyStyleMatchers version.
11
+ include ::Dommy::RSpec::CapyStyleMatchers
12
+ include Dommy::Rails::RSpec::Matchers
13
+ end
14
+ end
15
+ end
16
+ end