omniai-tools 0.5.0 → 0.5.1

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -12
  3. data/lib/omniai/tools/browser/base_driver.rb +78 -0
  4. data/lib/omniai/tools/browser/base_tool.rb +31 -4
  5. data/lib/omniai/tools/browser/button_click_tool.rb +1 -14
  6. data/lib/omniai/tools/browser/element_click_tool.rb +30 -0
  7. data/lib/omniai/tools/browser/elements/element_grouper.rb +73 -0
  8. data/lib/omniai/tools/browser/elements/nearby_element_detector.rb +108 -0
  9. data/lib/omniai/tools/browser/formatters/action_formatter.rb +37 -0
  10. data/lib/omniai/tools/browser/formatters/data_entry_formatter.rb +135 -0
  11. data/lib/omniai/tools/browser/formatters/element_formatter.rb +52 -0
  12. data/lib/omniai/tools/browser/formatters/input_formatter.rb +59 -0
  13. data/lib/omniai/tools/browser/inspect_tool.rb +46 -13
  14. data/lib/omniai/tools/browser/inspect_utils.rb +51 -0
  15. data/lib/omniai/tools/browser/link_click_tool.rb +2 -14
  16. data/lib/omniai/tools/browser/page_inspect/button_summarizer.rb +140 -0
  17. data/lib/omniai/tools/browser/page_inspect/form_summarizer.rb +98 -0
  18. data/lib/omniai/tools/browser/page_inspect/html_summarizer.rb +37 -0
  19. data/lib/omniai/tools/browser/page_inspect/link_summarizer.rb +103 -0
  20. data/lib/omniai/tools/browser/page_inspect_tool.rb +30 -0
  21. data/lib/omniai/tools/browser/page_screenshot_tool.rb +22 -0
  22. data/lib/omniai/tools/browser/selector_generator/base_selectors.rb +28 -0
  23. data/lib/omniai/tools/browser/selector_generator/contextual_selectors.rb +140 -0
  24. data/lib/omniai/tools/browser/selector_generator.rb +73 -0
  25. data/lib/omniai/tools/browser/selector_inspect_tool.rb +44 -0
  26. data/lib/omniai/tools/browser/text_field_area_set_tool.rb +2 -31
  27. data/lib/omniai/tools/browser/visit_tool.rb +1 -1
  28. data/lib/omniai/tools/browser/watir_driver.rb +224 -0
  29. data/lib/omniai/tools/browser_tool.rb +265 -0
  30. data/lib/omniai/tools/version.rb +1 -1
  31. metadata +23 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d27871f61c1d70be893cce085bf33c0b3d9280686ff1a0ba64f35835b7bf10d4
4
- data.tar.gz: 6dfddb639a3cbb99069cbb890bb5d8529bf152e5c246bcc90aaf344505758ea9
3
+ metadata.gz: cc814f27a50bb0ecceaff25eb4153aed6ade80ca24f7abcb74198e0a31afe627
4
+ data.tar.gz: c68820e0b080dc4812cd15871c8da74963d27b20960dd9e9e254b214bb313dfa
5
5
  SHA512:
6
- metadata.gz: ff02138674bc5176e869badb4268530d025e5543ca993eaea8aa74ee2ad114592bd5ac3e8ecd8ad8a882ec0af6dd57b9d74448b04669d3fe8295db3c9176e7b8
7
- data.tar.gz: c8a79aa2542b81475148b878ab7785d8bea1b32e4649718bd2a5d4684ba897e010da3d392ce9f3da38374f51e10ea2fc074a3a07a2392172a6c68e93f9298aa5
6
+ metadata.gz: 6ad4b2eb6e1cee4bbfd6b34f1957bb663de22785f4b2071de5323d38d5614c9438f813bbab9b98b9a232335a43243e82191103e59aca054400323f6137f5886e
7
+ data.tar.gz: faf87bbacaf66022c34beb835674deda75b359e937bc4a734918b8f2cc28985438eba994b40ac0553c8fb1ab7bfa433532cd4cbe0d6dffa3fa5298fa904565dd
data/README.md CHANGED
@@ -16,20 +16,11 @@ Database tools are focused on running SQL statements:
16
16
  require "omniai/openai"
17
17
  require "omniai/tools"
18
18
 
19
- require "watir"
20
-
21
- browser = Watir::Browser.new(:chrome)
22
-
23
19
  client = OmniAI::OpenAI::Client.new
24
- logger = Logger.new($stdout)
25
20
 
26
- tools = [
27
- OmniAI::Tools::Browser::VisitTool,
28
- OmniAI::Tools::Browser::InspectTool,
29
- OmniAI::Tools::Browser::ButtonClickTool,
30
- OmniAI::Tools::Browser::LinkClickTool,
31
- OmniAI::Tools::Browser::TextFieldAreaSetTool,
32
- ].map { |klass| klass.new(browser:, logger:) }
21
+ logger = Logger.new($stdout)
22
+ driver = OmniAI::Tools::Browser::WatirDriver.new
23
+ tools = [OmniAI::Tools::BrowserTool.new(driver: logger:)]
33
24
 
34
25
  puts "Type 'exit' or 'quit' to leave."
35
26
 
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ # A base driver intended to be overridden by specific browser drivers (e.g. waitir).
7
+ class BaseDriver
8
+ TIMEOUT = Integer(ENV.fetch("OMNIAI_BROWSER_TIMEOUT", 10))
9
+
10
+ # @param logger [Logger]
11
+ def initialize(logger: Logger.new(IO::NULL))
12
+ @logger = logger
13
+ end
14
+
15
+ def close
16
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
17
+ end
18
+
19
+ # @return [String]
20
+ def url
21
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
22
+ end
23
+
24
+ # @return [String]
25
+ def title
26
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
27
+ end
28
+
29
+ # @return [String]
30
+ def html
31
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
32
+ end
33
+
34
+ # @yield [file]
35
+ # @yieldparam file [File]
36
+ def screenshot
37
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
38
+ end
39
+
40
+ # @param url [String]
41
+ #
42
+ # @return [Hash]
43
+ def goto(url:)
44
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
45
+ end
46
+
47
+ # @param selector [String]
48
+ # @param text [String]
49
+ #
50
+ # @return [Hash]
51
+ def fill_in(selector:, text:)
52
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
53
+ end
54
+
55
+ # @param selector [String]
56
+ #
57
+ # @return [Hash]
58
+ def button_click(selector:)
59
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
60
+ end
61
+
62
+ # @param selector [String]
63
+ #
64
+ # @return [Hash]
65
+ def link_click(selector:)
66
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
67
+ end
68
+
69
+ # @param selector [String]
70
+ #
71
+ # @return [Hash]
72
+ def element_click(selector:)
73
+ raise NotImplementedError, "#{self.class.name}#{__method__} undefined"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -10,12 +10,39 @@ module OmniAI
10
10
  # # ...
11
11
  # end
12
12
  class BaseTool < OmniAI::Tool
13
- # @param logger [IO] An optional logger for debugging executed commands.
14
- # @param browser [Watir::Browser]
15
- def initialize(browser:, logger: Logger.new(IO::NULL))
13
+ # @param logger [Logger]
14
+ # @param driver [BaseDriver]
15
+ def initialize(driver:, logger: Logger.new(IO::NULL))
16
16
  super()
17
+ @driver = driver
17
18
  @logger = logger
18
- @browser = browser
19
+ end
20
+
21
+ protected
22
+
23
+ def wait_for_element
24
+ return yield if defined?(RSpec) # Skip waiting in tests
25
+
26
+ Watir::Wait.until(timeout: 10) do
27
+ element = yield
28
+ element if element && element_visible?(element)
29
+ end
30
+ rescue Watir::Wait::TimeoutError
31
+ log_element_timeout
32
+ nil
33
+ end
34
+
35
+ def element_visible?(element)
36
+ return true unless element.respond_to?(:visible?)
37
+
38
+ element.visible?
39
+ end
40
+
41
+ def log_element_timeout
42
+ return unless @browser.respond_to?(:elements)
43
+
44
+ visible_elements = @browser.elements.select(&:visible?).map(&:text).compact.first(10)
45
+ @logger.error("Element not found after 10s. Sample visible elements: #{visible_elements}")
19
46
  end
20
47
  end
21
48
  end
@@ -16,23 +16,10 @@ module OmniAI
16
16
 
17
17
  required %i[selector]
18
18
 
19
- # @param to [String] The URL to navigate to.
20
19
  def execute(selector:)
21
20
  @logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
22
21
 
23
- element = find(text: selector) || find(id: selector)
24
-
25
- return { error: "unknown selector=#{selector}" } if element.nil?
26
-
27
- element.click
28
- end
29
-
30
- protected
31
-
32
- # @return [Watir::Anchor, nil]
33
- def find(selector)
34
- element = @browser.button(selector)
35
- element if element.exists?
22
+ @driver.button_click(selector:)
36
23
  end
37
24
  end
38
25
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ # @example
7
+ # browser = Watir::Browser.new(:chrome)
8
+ # tool = OmniAI::Tools::Browser::ElementClickTool.new(browser:)
9
+ # tool.execute(selector: "#some-id")
10
+ # tool.execute(selector: ".some-class")
11
+ # tool.execute(selector: "some text")
12
+ # tool.execute(selector: "//div[@role='button']")
13
+ class ElementClickTool < BaseTool
14
+ description "A browser automation tool for clicking any clickable element."
15
+
16
+ parameter :selector, :string,
17
+ description: "CSS selector, ID, text content, or other identifier for the element to click."
18
+
19
+ required %i[selector]
20
+
21
+ # @param selector [String] CSS selector, ID, text content, or other identifier for the element to click.
22
+ def execute(selector:)
23
+ @logger.info("#{self.class.name}##{__method__} selector=#{selector.inspect}")
24
+
25
+ @driver.element_click(selector:)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ module Elements
7
+ # Groups HTML elements by their relevance for data entry
8
+ module ElementGrouper
9
+ module_function
10
+
11
+ def group_for_data_entry(elements, text)
12
+ groups = {
13
+ inputs: [], # text, number, email inputs - primary targets
14
+ form_controls: {}, # radio buttons, checkboxes grouped by name
15
+ labels: [], # labels, headers, spans
16
+ actions: [], # buttons and actionable elements
17
+ }
18
+
19
+ elements.each do |element|
20
+ categorize_element(element, groups, text)
21
+ end
22
+
23
+ groups
24
+ end
25
+
26
+ def categorize_element(element, groups, text)
27
+ case element.name.downcase
28
+ when "input"
29
+ categorize_input_element(element, groups)
30
+ when "textarea", "select"
31
+ groups[:inputs] << element
32
+ when "button"
33
+ groups[:actions] << element if contains_text_match?(element, text)
34
+ when "label", "span", "div", "th"
35
+ groups[:labels] << element if contains_text_match?(element, text)
36
+ end
37
+ end
38
+
39
+ def categorize_input_element(element, groups)
40
+ if data_entry_input?(element)
41
+ groups[:inputs] << element
42
+ else
43
+ group_form_control(element, groups[:form_controls])
44
+ end
45
+ end
46
+
47
+ def data_entry_input?(element)
48
+ # Handle missing type attribute - HTML default is "text"
49
+ type = (element["type"] || "text").downcase
50
+
51
+ # Include common data entry input types
52
+ %w[text number email tel url date datetime-local time month week
53
+ password search].include?(type)
54
+ end
55
+
56
+ def group_form_control(element, form_controls)
57
+ type = (element["type"] || "text").downcase
58
+ return unless %w[radio checkbox].include?(type)
59
+
60
+ name = element["name"] || "unnamed"
61
+ form_controls[name] ||= []
62
+ form_controls[name] << element
63
+ end
64
+
65
+ def contains_text_match?(element, text)
66
+ element.text.downcase.include?(text.downcase) ||
67
+ element["value"]&.downcase&.include?(text.downcase)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ module Elements
7
+ # Handles detection of interactive elements near text matches
8
+ module NearbyElementDetector
9
+ module_function
10
+
11
+ def add_nearby_interactive_elements(elements)
12
+ nearby_elements = []
13
+ elements.each do |element|
14
+ nearby_elements += find_interactive_in_context(element)
15
+ end
16
+ combined = elements.to_a + nearby_elements
17
+ combined.uniq
18
+ end
19
+
20
+ def find_interactive_in_context(element)
21
+ interactive_elements = []
22
+ interactive_elements += check_table_context(element)
23
+ interactive_elements += check_parent_containers(element)
24
+ interactive_elements.uniq
25
+ end
26
+
27
+ def check_table_context(element)
28
+ elements = []
29
+ table_cell = element.ancestors("td").first || element.ancestors("th").first
30
+ elements += find_interactive_in_container(table_cell) if table_cell
31
+ table_row = element.ancestors("tr").first
32
+ elements += find_interactive_in_container(table_row) if table_row
33
+ if table_cell && table_row
34
+ table = element.ancestors("table").first
35
+ elements += find_interactive_in_same_column(table, table_cell, table_row) if table
36
+ end
37
+ elements
38
+ end
39
+
40
+ def find_interactive_in_same_column(table, header_cell, header_row)
41
+ return [] unless table
42
+
43
+ column_index = header_row.css("th, td").index(header_cell)
44
+ return [] unless column_index
45
+
46
+ interactive_elements = []
47
+ table.css("tbody tr").each do |row|
48
+ interactive_elements += find_column_cell_elements(row, column_index)
49
+ end
50
+ interactive_elements
51
+ end
52
+
53
+ def find_column_cell_elements(row, column_index)
54
+ target_cell = row.css("td, th")[column_index]
55
+ return [] unless target_cell
56
+
57
+ find_interactive_in_container(target_cell) + find_interactive_in_nested_tables(target_cell, column_index)
58
+ end
59
+
60
+ def find_interactive_in_nested_tables(cell, original_column_index)
61
+ interactive_elements = []
62
+
63
+ cell.css("table").each do |nested_table|
64
+ nested_table.css("tr").each do |nested_row|
65
+ nested_cells = nested_row.css("td, th")
66
+ if nested_cells[original_column_index]
67
+ interactive_elements += find_interactive_in_container(nested_cells[original_column_index])
68
+ end
69
+ end
70
+ end
71
+
72
+ interactive_elements
73
+ end
74
+
75
+ def check_parent_containers(element)
76
+ interactive_elements = []
77
+ parent_container = element.parent
78
+ 3.times do
79
+ break unless parent_container
80
+
81
+ interactive_elements += find_interactive_in_container(parent_container)
82
+ parent_container = parent_container.parent
83
+ end
84
+ interactive_elements
85
+ end
86
+
87
+ def find_interactive_in_container(container)
88
+ return [] unless container
89
+
90
+ container.css("input, textarea, select, button, [role='button'], [tabindex='0']")
91
+ .reject { |el| non_interactive_element?(el) }
92
+ end
93
+
94
+ def non_interactive_element?(element)
95
+ return true if element["type"] == "hidden"
96
+ return true if element["tabindex"] == "-1"
97
+ return true if element["aria-hidden"] == "true"
98
+
99
+ style = element["style"]
100
+ return true if style&.include?("display: none") || style&.include?("visibility: hidden")
101
+
102
+ false
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ module Formatters
7
+ # Handles formatting of action elements (buttons, links)
8
+ module ActionFormatter
9
+ module_function
10
+
11
+ def format_actions(actions)
12
+ result = "⚡ Available Actions:\n"
13
+ actions.first(5).each do |action|
14
+ result += format_action_element(action)
15
+ end
16
+ result += " ... and #{actions.size - 5} more\n" if actions.size > 5
17
+ result += "\n"
18
+ end
19
+
20
+ def format_action_element(action)
21
+ text = action.text.strip
22
+ selector = get_action_selector(action)
23
+ " • #{text} (#{selector})\n"
24
+ end
25
+
26
+ def get_action_selector(action)
27
+ return action["id"] if action["id"] && !action["id"].empty?
28
+ return "text:#{action.text.strip}" if action.text.strip.length > 2
29
+ return action["class"].split.first if action["class"]
30
+
31
+ "css-needed"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ module Formatters
7
+ # Formats grouped elements for optimal data entry display
8
+ module DataEntryFormatter
9
+ module_function
10
+
11
+ def format_groups(grouped_elements, total_count, text)
12
+ result = build_header(total_count, text)
13
+ result += format_each_group(grouped_elements)
14
+ result
15
+ end
16
+
17
+ def format_inputs(inputs)
18
+ result = "📝 Data Entry Fields:\n"
19
+ table_groups = group_by_table_context(inputs)
20
+ result += format_table_groups(table_groups)
21
+ "#{result}\n"
22
+ end
23
+
24
+ def format_form_controls(form_controls)
25
+ result = "🎛️ Form Controls:\n"
26
+ form_controls.each do |name, controls|
27
+ result += format_control_group(name, controls)
28
+ end
29
+ result += "\n"
30
+ end
31
+
32
+ def format_labels(labels)
33
+ result = "🏷️ Labels & Headers:\n"
34
+ labels.first(3).each do |label|
35
+ result += " • #{label.name}: #{label.text.strip}\n"
36
+ end
37
+ result += " ... and #{labels.size - 3} more\n" if labels.size > 3
38
+ result += "\n"
39
+ end
40
+
41
+ def build_header(total_count, text)
42
+ "Found #{total_count} elements containing '#{text}':\n\n"
43
+ end
44
+
45
+ def format_each_group(grouped_elements)
46
+ [
47
+ format_inputs_section(grouped_elements[:inputs]),
48
+ format_actions_section(grouped_elements[:actions]),
49
+ format_controls_section(grouped_elements[:form_controls]),
50
+ format_labels_section(grouped_elements[:labels]),
51
+ ].join
52
+ end
53
+
54
+ def format_inputs_section(inputs)
55
+ inputs.any? ? format_inputs(inputs) : ""
56
+ end
57
+
58
+ def format_actions_section(actions)
59
+ actions.any? ? ActionFormatter.format_actions(actions) : ""
60
+ end
61
+
62
+ def format_controls_section(form_controls)
63
+ form_controls.any? ? format_form_controls(form_controls) : ""
64
+ end
65
+
66
+ def format_labels_section(labels)
67
+ labels.any? ? format_labels(labels) : ""
68
+ end
69
+
70
+ def format_control_group(name, controls)
71
+ result = "\n #{name.humanize} options:\n"
72
+ controls.each do |control|
73
+ checked = control["checked"] ? " \u2713" : ""
74
+ result += " • #{control['value']}#{checked}\n"
75
+ end
76
+ result
77
+ end
78
+
79
+ def format_table_groups(table_groups)
80
+ return format_single_group(table_groups.values.first) if table_groups.size == 1
81
+
82
+ result = ""
83
+ table_groups.each do |(context, group_inputs)|
84
+ result += "\n #{context}:\n"
85
+ group_inputs.each { |input| result += InputFormatter.format_input_field(input, " ") }
86
+ end
87
+ result
88
+ end
89
+
90
+ def format_single_group(inputs)
91
+ result = ""
92
+ inputs.each { |input| result += InputFormatter.format_input_field(input, " ") }
93
+ result
94
+ end
95
+
96
+ def group_by_table_context(inputs)
97
+ groups = {}
98
+ inputs.each do |input|
99
+ context = find_context(input)
100
+ groups[context] ||= []
101
+ groups[context] << input
102
+ end
103
+ groups
104
+ end
105
+
106
+ def find_context(input)
107
+ table = input.ancestors("table").first
108
+ return "Form Fields" unless table
109
+
110
+ find_table_context(input)
111
+ end
112
+
113
+ def find_table_context(input)
114
+ row = input.ancestors("tr").first
115
+ return "Table" unless row
116
+
117
+ find_meaningful_cell_text(row)
118
+ end
119
+
120
+ def find_meaningful_cell_text(row)
121
+ meaningful_cell = row.css("td, th").find do |cell|
122
+ cell.text.strip.length > 2 && !cell.text.match?(/^\d+\.?\d*$/)
123
+ end
124
+
125
+ meaningful_cell ? meaningful_cell.text.strip[0..30] : "Table"
126
+ end
127
+
128
+ def humanize(text)
129
+ text.gsub(/[-_]/, " ").split.map(&:capitalize).join(" ")
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ module Formatters
7
+ # Handles formatting of HTML elements for display with data entry focus
8
+ module ElementFormatter
9
+ module_function
10
+
11
+ def format_matching_elements(elements, text, _context_size = nil)
12
+ grouped = Elements::ElementGrouper.group_for_data_entry(elements, text)
13
+ DataEntryFormatter.format_groups(grouped, elements.size, text)
14
+ end
15
+
16
+ # Keep existing methods for backward compatibility
17
+ def format_single_element(element, index, context_size)
18
+ result = "--- Element #{index + 1} ---\n"
19
+ result += "Tag: #{element.name}\n"
20
+ result += format_element_attributes(element)
21
+ result += get_parent_context(element, context_size) if context_size.positive?
22
+ result += "HTML: #{element.to_html}\n\n"
23
+ result
24
+ end
25
+
26
+ def format_element_attributes(element)
27
+ result = ""
28
+ result += "ID: #{element['id']}\n" if element["id"]
29
+ result += "Classes: #{element['class']}\n" if element["class"]
30
+ %w[href src alt type value placeholder].each do |attr|
31
+ result += "#{attr}: #{element[attr]}\n" if element[attr] && !element[attr].empty?
32
+ end
33
+ result
34
+ end
35
+
36
+ def get_parent_context(element, context_size)
37
+ result = ""
38
+ parent = element.parent
39
+ context_count = 0
40
+ while parent && context_count < context_size
41
+ attrs = parent.attributes.map { |name, attr| " #{name}=\"#{attr.value}\"" }.join
42
+ result += "Parent #{context_count + 1}: <#{parent.name}#{attrs}>\n"
43
+ parent = parent.parent
44
+ context_count += 1
45
+ end
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Tools
5
+ module Browser
6
+ module Formatters
7
+ # Handles formatting of input elements
8
+ module InputFormatter
9
+ module_function
10
+
11
+ def format_input_field(input, indent = "")
12
+ result = format_basic_line(input, indent)
13
+ result += format_selectors_line(input, indent)
14
+ result
15
+ end
16
+
17
+ def format_basic_line(input, indent)
18
+ result = "#{indent}• #{input_type_display(input)}"
19
+ result += input_id_display(input)
20
+ result += input_value_display(input)
21
+ result += input_placeholder_display(input)
22
+ "#{result}\n"
23
+ end
24
+
25
+ def format_selectors_line(input, indent)
26
+ selectors = SelectorGenerator.generate_stable_selectors(input)
27
+ return "" if selectors.empty?
28
+
29
+ format_selector_list(selectors, indent)
30
+ end
31
+
32
+ def format_selector_list(selectors, indent)
33
+ result = "#{indent} Stable selectors:\n"
34
+ selectors.each { |sel| result += "#{indent} - #{sel}\n" }
35
+ "#{result}\n"
36
+ end
37
+
38
+ def input_type_display(input)
39
+ (input["type"] || input.name).capitalize
40
+ end
41
+
42
+ def input_id_display(input)
43
+ input["id"] ? " (#{input['id']})" : ""
44
+ end
45
+
46
+ def input_value_display(input)
47
+ value = input["value"]
48
+ value && !value.empty? ? " = '#{value}'" : ""
49
+ end
50
+
51
+ def input_placeholder_display(input)
52
+ placeholder = input["placeholder"]
53
+ placeholder && !placeholder.empty? ? " [#{placeholder}]" : ""
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end