Module: OmniAI::Tools::Browser::SelectorGenerator::ContextualSelectors

Included in:
OmniAI::Tools::Browser::SelectorGenerator
Defined in:
lib/omniai/tools/browser/selector_generator/contextual_selectors.rb

Overview

Context-aware selector generation for complex elements

Instance Method Summary collapse

Instance Method Details

#build_parent_selector(element, parent_class) ⇒ Object

Build selector with parent class context



29
30
31
32
33
34
35
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 29

def build_parent_selector(element, parent_class)
  base = ".#{parent_class} #{element.name}"
  return ["#{base}[placeholder=\"#{element['placeholder']}\"]"] if element["placeholder"]
  return ["#{base}[type=\"#{element['type']}\"]"] if element["type"]

  [base]
end

#build_position_selector(element, index, parent_context = "") ⇒ Object

Build nth-of-type selector



103
104
105
106
107
108
109
110
111
112
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 103

def build_position_selector(element, index, parent_context = "")
  nth = ":nth-of-type(#{index})"
  base = "#{parent_context}#{element.name}#{nth}"
  return ["#{parent_context}#{element.name}[type=\"#{element['type']}\"]#{nth}"] if element["type"]
  if element["placeholder"]
    return ["#{parent_context}#{element.name}[placeholder=\"#{element['placeholder']}\"]#{nth}"]
  end

  [base]
end

#element_has_significant_class?(element) ⇒ Boolean

Check if element has significant class

Returns:

  • (Boolean)


49
50
51
52
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 49

def element_has_significant_class?(element)
  classes = element["class"]&.split || []
  classes.any? { |c| significant_class?(c) }
end

#find_label_for_element(element) ⇒ Object

Find label element associated with this element



88
89
90
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 88

def find_label_for_element(element)
  element.document.at_css("label[for=\"#{element['id']}\"]")
end

#find_significant_parent(element) ⇒ Object

Find parent with meaningful class (not generic like ‘row’ or ‘col’)



38
39
40
41
42
43
44
45
46
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 38

def find_significant_parent(element)
  parent = element.parent
  while parent && parent.name != "body"
    return parent if element_has_significant_class?(parent)

    parent = parent.parent
  end
  nil
end

#find_similar_siblings(element) ⇒ Object

Find sibling elements of same type with similar attributes



115
116
117
118
119
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 115

def find_similar_siblings(element)
  return [] unless element.parent

  element.parent.css(element.name).select { |sibling| same_key_attributes?(element, sibling) }
end

#generate_contextual_selectors(element) ⇒ Object



9
10
11
12
13
14
15
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 9

def generate_contextual_selectors(element)
  selectors = []
  selectors.concat(parent_class_selectors(element))
  selectors.concat(label_based_selectors(element))
  selectors.concat(position_based_selectors(element))
  selectors
end

#generic_class?(class_name) ⇒ Boolean

Common generic class names to ignore

Returns:

  • (Boolean)


63
64
65
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 63

def generic_class?(class_name)
  %w[row col container wrapper inner outer main].include?(class_name.downcase)
end

#label_based_selectors(element) ⇒ Object

Generate selectors based on label associations



74
75
76
77
78
79
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 74

def label_based_selectors(element)
  return [] unless stable_id?(element)

  label = find_label_for_element(element)
  label ? ["#{element.name}##{element['id']}"] : []
end

#most_specific_class(element) ⇒ Object

Get most specific (longest) class name



68
69
70
71
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 68

def most_specific_class(element)
  classes = element["class"]&.split || []
  classes.select { |c| significant_class?(c) }.max_by(&:length)
end

#parent_class_selectors(element) ⇒ Object

Generate selectors based on parent container classes



18
19
20
21
22
23
24
25
26
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 18

def parent_class_selectors(element)
  significant_parent = find_significant_parent(element)
  return [] unless significant_parent

  parent_class = most_specific_class(significant_parent)
  return [] unless parent_class

  build_parent_selector(element, parent_class)
end

#parent_context_prefix(element) ⇒ Object

Get parent context for more specific position selectors



129
130
131
132
133
134
135
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 129

def parent_context_prefix(element)
  parent = find_significant_parent(element)
  return "" unless parent

  parent_class = most_specific_class(parent)
  parent_class ? ".#{parent_class} " : ""
end

#position_based_selectors(element) ⇒ Object

Generate position-based selectors for similar elements



93
94
95
96
97
98
99
100
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 93

def position_based_selectors(element)
  siblings = find_similar_siblings(element)
  return [] unless siblings.size > 1

  index = siblings.index(element) + 1
  parent_context = parent_context_prefix(element)
  build_position_selector(element, index, parent_context)
end

#same_key_attributes?(elem1, elem2) ⇒ Boolean

Check if two elements have same key attributes

Returns:

  • (Boolean)


122
123
124
125
126
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 122

def same_key_attributes?(elem1, elem2)
  return false unless elem1.name == elem2.name

  elem1.name == "input" ? elem1["type"] == elem2["type"] : true
end

#significant_class?(class_name) ⇒ Boolean

Check if class name is likely to be meaningful/specific

Returns:

  • (Boolean)


55
56
57
58
59
60
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 55

def significant_class?(class_name)
  return false if class_name.length < 4
  return false if generic_class?(class_name)

  class_name.match?(/[a-z]+[-_]?[a-z]+/i)
end

#stable_id?(element) ⇒ Boolean

Check if element has stable (non-React) ID

Returns:

  • (Boolean)


82
83
84
85
# File 'lib/omniai/tools/browser/selector_generator/contextual_selectors.rb', line 82

def stable_id?(element)
  id = element["id"]
  id && !id.empty? && !id.match?(/^:r[0-9a-z]+:$/i)
end