Module: Dommy::Interaction::DomSummary

Defined in:
lib/dommy/interaction/dom_summary.rb

Overview

Summarizes the user-facing controls of a DOM scope (a document or any element with query_selector_all): the buttons, fields, links, and forms a person would see. Engine-independent and scope-general — it backs the finder's "available …" failure messages, the Debug facade, and failure artifacts, and can be called directly on any document (e.g. a request spec's dom).

Dommy::Interaction::DomSummary.buttons(document)
#=> [{label: "Save", type: "submit", selector: "button[type=submit]"}]
puts Dommy::Interaction::DomSummary.to_text(document)

Class Method Summary collapse

Class Method Details

.button_label(el) ⇒ Object

--- label / descriptor helpers ---



77
78
79
80
81
82
83
# File 'lib/dommy/interaction/dom_summary.rb', line 77

def button_label(el)
  if el.tag_name == "INPUT"
    presence(el.get_attribute("value")) || presence(el.get_attribute("alt")) || ""
  else
    squish(el.text_content)
  end
end

.button_labels(scope) ⇒ Object

One-line descriptors for failure messages: "Save" button[type=submit].



57
# File 'lib/dommy/interaction/dom_summary.rb', line 57

def button_labels(scope) = buttons(scope).map { |b| "#{quote(b[:label])} #{b[:selector]}".strip }

.buttons(scope) ⇒ Object

Submit-capable and plain buttons, as type:, selector:.



19
20
21
22
23
24
25
# File 'lib/dommy/interaction/dom_summary.rb', line 19

def buttons(scope)
  return [] unless scope

  scope.query_selector_all("button, input[type='submit'], input[type='image'], input[type='button'], input[type='reset']").map do |el|
    {label: button_label(el), type: presence(el.get_attribute("type")), selector: selector_for(el)}
  end
end

.field_label(el, scope) ⇒ Object

Prefer an associated



86
87
88
89
90
# File 'lib/dommy/interaction/dom_summary.rb', line 86

def field_label(el, scope)
  label_caption(el, scope) || presence(el.get_attribute("aria-label")) ||
    presence(el.get_attribute("placeholder")) || presence(el.get_attribute("name")) ||
    presence(el.get_attribute("id"))
end

.field_labels(scope) ⇒ Object



58
# File 'lib/dommy/interaction/dom_summary.rb', line 58

def field_labels(scope) = fields(scope).map { |f| "#{quote(f[:label] || f[:name])} #{f[:selector]}".strip }

.field_type(el) ⇒ Object



99
100
101
102
103
# File 'lib/dommy/interaction/dom_summary.rb', line 99

def field_type(el)
  return el.tag_name.downcase unless el.tag_name == "INPUT"

  presence(el.get_attribute("type")) || "text"
end

.fields(scope) ⇒ Object

Form fields, as name:, id:, type:, selector:.



28
29
30
31
32
33
34
35
# File 'lib/dommy/interaction/dom_summary.rb', line 28

def fields(scope)
  return [] unless scope

  scope.query_selector_all("input, textarea, select").reject { |el| hidden_input?(el) }.map do |el|
    {label: field_label(el, scope), name: presence(el.get_attribute("name")),
     id: presence(el.get_attribute("id")), type: field_type(el), selector: selector_for(el)}
  end
end

.form_method(form) ⇒ Object



125
# File 'lib/dommy/interaction/dom_summary.rb', line 125

def form_method(form) = presence(form.get_attribute("method"))&.downcase

.forms(scope) ⇒ Object

Forms, as method:, fields: [names].



47
48
49
50
51
52
53
54
# File 'lib/dommy/interaction/dom_summary.rb', line 47

def forms(scope)
  return [] unless scope

  scope.query_selector_all("form").map do |form|
    {action: presence(form.get_attribute("action")), method: form_method(form),
     fields: fields(form).map { |f| f[:name] }.compact}
  end
end

.hidden_input?(el) ⇒ Boolean

Returns:

  • (Boolean)


124
# File 'lib/dommy/interaction/dom_summary.rb', line 124

def hidden_input?(el) = el.tag_name == "INPUT" && el.get_attribute("type") == "hidden"

.label_caption(el, scope) ⇒ Object



92
93
94
95
96
97
# File 'lib/dommy/interaction/dom_summary.rb', line 92

def label_caption(el, scope)
  id = presence(el.get_attribute("id"))
  wrapping = el.respond_to?(:closest) ? el.closest("label") : nil
  label = (id && scope.query_selector_all("label").find { |l| l.get_attribute("for") == id }) || wrapping
  label ? presence(squish(label.text_content)) : nil
end


59
# File 'lib/dommy/interaction/dom_summary.rb', line 59

def link_labels(scope) = links(scope).map { |l| "#{quote(l[:text])} -> #{l[:href]}" }

Links with an href, as href:.



38
39
40
41
42
43
44
# File 'lib/dommy/interaction/dom_summary.rb', line 38

def links(scope)
  return [] unless scope

  scope.query_selector_all("a[href]").map do |el|
    {text: squish(el.text_content), href: el.get_attribute("href").to_s}
  end
end

.presence(value) ⇒ Object



127
# File 'lib/dommy/interaction/dom_summary.rb', line 127

def presence(value) = (value.nil? || value.to_s.empty?) ? nil : value

.quote(value) ⇒ Object



128
# File 'lib/dommy/interaction/dom_summary.rb', line 128

def quote(value) = value.to_s.inspect

.selector_for(el) ⇒ Object



105
106
107
108
109
110
111
112
113
# File 'lib/dommy/interaction/dom_summary.rb', line 105

def selector_for(el)
  tag = el.tag_name.downcase
  type = presence(el.get_attribute("type"))
  name = presence(el.get_attribute("name"))
  descriptor = tag
  descriptor += "[type=#{type}]" if type
  descriptor += "[name=#{name}]" if name
  descriptor
end

.squish(text) ⇒ Object



126
# File 'lib/dommy/interaction/dom_summary.rb', line 126

def squish(text) = text.to_s.gsub(/\s+/, " ").strip

.text(scope) ⇒ Object

Collapsed visible text of a scope. A Document has no text_content of its own, so read its .



117
118
119
120
121
122
# File 'lib/dommy/interaction/dom_summary.rb', line 117

def text(scope)
  return "" unless scope

  node = scope.respond_to?(:body) ? scope.body : scope
  squish(node&.text_content)
end

.to_text(scope) ⇒ Object

A readable, sectioned summary of the scope's controls (used by the Debug facade and failure artifacts).



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/dommy/interaction/dom_summary.rb', line 63

def to_text(scope)
  sections = {
    "Forms" => forms(scope).map { |f| "#{(f[:method] || "get").upcase} #{f[:action] || "(self)"} [#{f[:fields].join(", ")}]" },
    "Links" => link_labels(scope),
    "Buttons" => button_labels(scope),
    "Fields" => field_labels(scope)
  }
  sections.reject { |_, rows| rows.empty? }
    .map { |name, rows| "#{name}:\n#{rows.map { |r| "  #{r}" }.join("\n")}" }
    .join("\n")
end