Module: Dommy::Internal::DomMatching
- Defined in:
- lib/dommy/internal/dom_matching.rb
Overview
Shared matching primitives used by both RSpec matchers and Minitest assertions. Centralizes selector / text / count interpretation so the two frameworks behave identically.
Constant Summary collapse
- INLINE_ZERO_OPACITY =
opacity: 0 / 0.0 / .0 / 0% (and nothing else) as an inline value.
/opacity\s*:\s*\+?(?:0+(?:\.0*)?|\.0+)%?\s*(?:[;!]|\z)/i- NON_RENDERED_TEXT_TAGS =
Tags whose text never renders, so it must not count as page text.
%w[head script style template noscript].freeze
Class Method Summary collapse
- .count_matches?(actual, expected) ⇒ Boolean
-
.css_visible?(element) ⇒ Boolean
CSS-aware extension of visible?, consulted only when the document has author CSS (Cascade.author_css? keeps sheetless documents on the fast path).
-
.filter(scope, selector, text: nil) ⇒ Array<Dommy::Element>
Find elements in scope matching selector, optionally filtered by text content.
-
.filter_by_visibility(elements, visible) ⇒ Object
Filter elements by Capybara-style :visible option.
-
.html_of(scope) ⇒ Object
Get the inner_html of a scope, falling back to body for Document.
-
.normalize_html(html) ⇒ Object
Normalize an HTML string for structural comparison.
- .opacity_zero?(value) ⇒ Boolean
-
.rendered_text(node) ⇒ Object
Visible text of a node's subtree: like
text_content, but excluding subtrees that never render (script/style/head/template/noscript). - .text_matches?(actual, expected, exact: false) ⇒ Boolean
-
.text_of(scope) ⇒ Object
Get the text_content of a scope, handling Document (which has no text_content directly — its body does).
-
.visible?(element) ⇒ Boolean
Visibility check.
Class Method Details
.count_matches?(actual, expected) ⇒ Boolean
41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/dommy/internal/dom_matching.rb', line 41 def count_matches?(actual, expected) case expected when nil actual.positive? when Integer actual == expected when ::Range expected.cover?(actual) else false end end |
.css_visible?(element) ⇒ Boolean
CSS-aware extension of visible?, consulted only when the document
has author CSS (Cascade.author_css? keeps sheetless documents on the
fast path). display: none on the element or any ancestor hides;
computed visibility: hidden/collapse (inherited, overridable by a
descendant's visibility: visible) hides.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/dommy/internal/dom_matching.rb', line 112 def css_visible?(element) document = element.respond_to?(:owner_document) ? element.owner_document : nil return true unless document && CSS::Cascade.(document) return false if %w[hidden collapse].include?(CSS::Cascade.computed_style(element)["visibility"]) current = element while current styles = CSS::Cascade.computed_style(current) return false if styles["display"] == "none" # Zero effective opacity is invisible (Selenium's displayed # algorithm); a zero anywhere in the chain zeroes the product. return false if opacity_zero?(styles["opacity"]) current = current.respond_to?(:parent_element) ? current.parent_element : nil end true end |
.filter(scope, selector, text: nil) ⇒ Array<Dommy::Element>
Find elements in scope matching selector, optionally filtered by text content.
18 19 20 21 22 23 |
# File 'lib/dommy/internal/dom_matching.rb', line 18 def filter(scope, selector, text: nil) elements = scope.query_selector_all(selector).to_a return elements if text.nil? elements.select { |el| text_matches?(el.text_content, text) } end |
.filter_by_visibility(elements, visible) ⇒ Object
Filter elements by Capybara-style :visible option.
141 142 143 144 145 146 147 148 149 150 |
# File 'lib/dommy/internal/dom_matching.rb', line 141 def filter_by_visibility(elements, visible) case visible when nil, :all, false elements when :hidden elements.reject { |el| visible?(el) } else elements.select { |el| visible?(el) } end end |
.html_of(scope) ⇒ Object
Get the inner_html of a scope, falling back to body for Document.
76 77 78 79 80 81 82 83 84 |
# File 'lib/dommy/internal/dom_matching.rb', line 76 def html_of(scope) if scope.respond_to?(:inner_html) scope.inner_html.to_s elsif scope.respond_to?(:body) && scope.body scope.body.inner_html.to_s else scope.to_s end end |
.normalize_html(html) ⇒ Object
Normalize an HTML string for structural comparison. Re-parses through Nokogiri and re-serializes, which collapses whitespace differences and attribute ordering quirks.
59 60 61 |
# File 'lib/dommy/internal/dom_matching.rb', line 59 def normalize_html(html) Backend.fragment(html.to_s, owner_doc: nil).to_html.gsub(/\s+/, " ").strip end |
.opacity_zero?(value) ⇒ Boolean
131 132 133 134 135 136 |
# File 'lib/dommy/internal/dom_matching.rb', line 131 def opacity_zero?(value) text = value.to_s.strip return false unless text.match?(/\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)%?\z/) text.to_f <= 0 end |
.rendered_text(node) ⇒ Object
Visible text of a node's subtree: like text_content, but excluding
subtrees that never render (script/style/head/template/noscript).
Mirrors what a browser exposes to has_text? / text filters, so JSON
embedded in a <script data-page> (Inertia/Turbo) or inline CSS is not
mistaken for visible page text.
184 185 186 187 188 189 190 |
# File 'lib/dommy/internal/dom_matching.rb', line 184 def rendered_text(node) return "" if node.nil? out = +"" append_rendered_text(node, out) out end |
.text_matches?(actual, expected, exact: false) ⇒ Boolean
29 30 31 32 33 34 35 36 37 |
# File 'lib/dommy/internal/dom_matching.rb', line 29 def text_matches?(actual, expected, exact: false) actual = actual.to_s case expected when Regexp exact ? actual.match?(expected) && actual == actual[expected] : actual.match?(expected) else exact ? actual.strip == expected.to_s : actual.include?(expected.to_s) end end |
.text_of(scope) ⇒ Object
Get the text_content of a scope, handling Document (which has no text_content directly — its body does).
65 66 67 68 69 70 71 72 73 |
# File 'lib/dommy/internal/dom_matching.rb', line 65 def text_of(scope) if scope.respond_to?(:text_content) scope.text_content.to_s elsif scope.respond_to?(:body) && scope.body scope.body.text_content.to_s else scope.to_s end end |
.visible?(element) ⇒ Boolean
Visibility check. Fast HTML-level signals first (hidden attribute,
<input type=hidden>, non-rendering ancestors, inline
display:none / visibility:hidden); when the document carries
author CSS and the makiri-backed parser is available, stylesheet-
driven display:none / visibility:hidden (e.g. via a class) is
detected through the computed style as well. No layout: geometry-
dependent invisibility stays out of scope.
93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/dommy/internal/dom_matching.rb', line 93 def visible?(element) return true unless element.respond_to?(:__dommy_backend_node__) node = element.__dommy_backend_node__ return false if node_invisible_self?(node) NodeTraversal.each_ancestor(node) do |ancestor| return false if non_rendering_tag?(ancestor) return false if node_invisible_self?(ancestor) end css_visible?(element) end |