appium_failure_helper 1.4.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b1148ccbdea9b7b8446bf489a8f325e6dd4255cbeeba75ed58469f21169a126
4
- data.tar.gz: 646ef386ee881d2f9cccf4ca3e85f8a79bd3cd132221c537e6db3f07c719f91f
3
+ metadata.gz: 3ef5fd316590ddca6a0b9ccc21b06f947c3b439fb998668b2b965ae5f110702a
4
+ data.tar.gz: af17ce648f330a535cd5d0582fd4441f2cbcc881fd67626e8516a202655e747e
5
5
  SHA512:
6
- metadata.gz: f345294b82fad66aa0b47e3455a85063caef1f292225f9954d5bd10a0370f29e9ba51c15d7caeed219c4e587b84f115e4e762e7b16f9408bacdc91dbce8aaf55
7
- data.tar.gz: 505c2e690da3d723c5afb43a7afb1f219bca682e45e1c8c14068c98072c1331c90fecbe764312fe4ad3071d95df57106068b4feea5c4ca68c736b5fcbcd647dd
6
+ metadata.gz: bec158f942080f960031179836e9830ea7f3fd2cc48117d5b803d5156d0ed89ee27bf1377c10a36f7ec4be92aeea7c9eac60e2232c77e75a5c19fd390b9fb306
7
+ data.tar.gz: 66a21888247d7bb983556428f3aad65dfa8fd9b3f576d670bc736a4db1e520b9874b2b01628b9fc7f3ad94dd06363d2d0faacb78c6ad042d373eb60c75ffcd84
data/.idea/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -0,0 +1,48 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$">
8
+ <sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
9
+ <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
10
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
11
+ </content>
12
+ <orderEntry type="inheritedJdk" />
13
+ <orderEntry type="sourceFolder" forTests="false" />
14
+ <orderEntry type="library" scope="PROVIDED" name="appium_lib (v10.6.0, ruby-3.1.7-p261) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="appium_lib_core (v3.11.1, ruby-3.1.7-p261) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, ruby-3.1.7-p261) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.8, ruby-3.1.7-p261) [gem]" level="application" />
18
+ <orderEntry type="library" scope="PROVIDED" name="cgi (v0.5.0, ruby-3.1.7-p261) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="childprocess (v3.0.0, ruby-3.1.7-p261) [gem]" level="application" />
20
+ <orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, ruby-3.1.7-p261) [gem]" level="application" />
21
+ <orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, ruby-3.1.7-p261) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="erb (v4.0.4, ruby-3.1.7-p261) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="eventmachine (v1.2.7, ruby-3.1.7-p261) [gem]" level="application" />
24
+ <orderEntry type="library" scope="PROVIDED" name="faye-websocket (v0.11.4, ruby-3.1.7-p261) [gem]" level="application" />
25
+ <orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.2, ruby-3.1.7-p261) [gem]" level="application" />
26
+ <orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.1, ruby-3.1.7-p261) [gem]" level="application" />
27
+ <orderEntry type="library" scope="PROVIDED" name="irb (v1.15.2, ruby-3.1.7-p261) [gem]" level="application" />
28
+ <orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.18.10, ruby-3.1.7-p261) [gem]" level="application" />
29
+ <orderEntry type="library" scope="PROVIDED" name="pp (v0.6.2, ruby-3.1.7-p261) [gem]" level="application" />
30
+ <orderEntry type="library" scope="PROVIDED" name="prettyprint (v0.2.0, ruby-3.1.7-p261) [gem]" level="application" />
31
+ <orderEntry type="library" scope="PROVIDED" name="psych (v5.2.6, ruby-3.1.7-p261) [gem]" level="application" />
32
+ <orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, ruby-3.1.7-p261) [gem]" level="application" />
33
+ <orderEntry type="library" scope="PROVIDED" name="rake (v13.3.0, ruby-3.1.7-p261) [gem]" level="application" />
34
+ <orderEntry type="library" scope="PROVIDED" name="rdoc (v6.14.2, ruby-3.1.7-p261) [gem]" level="application" />
35
+ <orderEntry type="library" scope="PROVIDED" name="reline (v0.6.2, ruby-3.1.7-p261) [gem]" level="application" />
36
+ <orderEntry type="library" scope="PROVIDED" name="rspec (v3.13.1, ruby-3.1.7-p261) [gem]" level="application" />
37
+ <orderEntry type="library" scope="PROVIDED" name="rspec-core (v3.13.5, ruby-3.1.7-p261) [gem]" level="application" />
38
+ <orderEntry type="library" scope="PROVIDED" name="rspec-expectations (v3.13.5, ruby-3.1.7-p261) [gem]" level="application" />
39
+ <orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.13.5, ruby-3.1.7-p261) [gem]" level="application" />
40
+ <orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.13.6, ruby-3.1.7-p261) [gem]" level="application" />
41
+ <orderEntry type="library" scope="PROVIDED" name="rubyzip (v3.1.0, ruby-3.1.7-p261) [gem]" level="application" />
42
+ <orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v3.142.7, ruby-3.1.7-p261) [gem]" level="application" />
43
+ <orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.7, ruby-3.1.7-p261) [gem]" level="application" />
44
+ <orderEntry type="library" scope="PROVIDED" name="tomlrb (v1.3.0, ruby-3.1.7-p261) [gem]" level="application" />
45
+ <orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.7.7, ruby-3.1.7-p261) [gem]" level="application" />
46
+ <orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, ruby-3.1.7-p261) [gem]" level="application" />
47
+ </component>
48
+ </module>
data/.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="ruby-3.1.7-p261" project-jdk-type="RUBY_SDK" />
4
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/Appium_failure_helper.iml" filepath="$PROJECT_DIR$/.idea/Appium_failure_helper.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
data/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -2,12 +2,10 @@ module AppiumFailureHelper
2
2
  module Analyzer
3
3
  def self.triage_error(exception)
4
4
  case exception
5
- when Selenium::WebDriver::Error::NoSuchElementError,
6
- Selenium::WebDriver::Error::TimeoutError,
7
- Selenium::WebDriver::Error::UnknownCommandError
8
- :locator_issue
9
- when Selenium::WebDriver::Error::TimeoutError,
10
- Appium::Core::Wait::TimeoutError
5
+ when Selenium::WebDriver::Error::NoSuchElementError,
6
+ Selenium::WebDriver::Error::TimeoutError,
7
+ Selenium::WebDriver::Error::UnknownCommandError,
8
+ defined?(Appium::Core::Wait::TimeoutError) ? Appium::Core::Wait::TimeoutError : nil
11
9
  :locator_issue
12
10
  when Selenium::WebDriver::Error::ElementNotInteractableError
13
11
  :visibility_issue
@@ -30,69 +28,52 @@ module AppiumFailureHelper
30
28
  def self.extract_failure_details(exception)
31
29
  message = exception.message.to_s
32
30
  info = {}
33
- patterns = [
34
- /using "([^"]+)" with value "([^"]+)"/,
35
- /element with locator ['"]?(#?\w+)['"]?/i,
36
- /(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
37
- /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
38
- ]
39
- patterns.each do |pattern|
40
- match = message.match(pattern)
41
- if match
42
- if match.captures.size == 2
43
- info[:selector_type], info[:selector_value] = match.captures.map(&:strip)
44
- else
45
- info[:selector_value] = match.captures.first.strip
46
- info[:selector_type] = 'logical_name'
47
- end
48
- return info
49
- end
31
+ pattern = /using "([^"]+)" with value "([^"]+)"/
32
+ match = message.match(pattern)
33
+ if match
34
+ info[:selector_type], info[:selector_value] = match.captures
50
35
  end
51
36
  info
52
37
  end
53
-
54
- def self.find_de_para_match(failed_info, element_map)
55
- failed_value = (failed_info || {})[:selector_value].to_s
56
- return nil if failed_value.empty?
57
- logical_name_key = failed_value.gsub(/^#/, '')
58
- if element_map.key?(logical_name_key)
59
- return { logical_name: logical_name_key, correct_locator: element_map[logical_name_key] }
60
- end
61
- cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
62
- element_map.each do |name, locator_info|
63
- mapped_locator = (locator_info || {})['valor'].to_s
64
- cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
65
- distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
66
- max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
67
- next if max_len.zero?
68
- similarity_score = 1.0 - (distance.to_f / max_len)
69
- if similarity_score > 0.85
70
- return { logical_name: name, correct_locator: locator_info }
38
+
39
+ def self.perform_advanced_analysis(failed_info, all_page_elements, platform)
40
+ return nil if failed_info.empty? || all_page_elements.empty?
41
+ expected_attrs = parse_locator(failed_info[:selector_type], failed_info[:selector_value], platform)
42
+ return nil if expected_attrs.empty?
43
+
44
+ id_key_to_check = (platform.to_s == 'ios') ? 'name' : 'resource-id'
45
+ candidates = all_page_elements.map do |element_on_screen|
46
+ score = 0
47
+ analysis = {}
48
+
49
+ if expected_attrs[id_key_to_check]
50
+ actual_id = element_on_screen[:attributes][id_key_to_check]
51
+ distance = DidYouMean::Levenshtein.distance(expected_attrs[id_key_to_check].to_s, actual_id.to_s)
52
+ max_len = [expected_attrs[id_key_to_check].to_s.length, actual_id.to_s.length].max
53
+ similarity = max_len.zero? ? 0 : 1.0 - (distance.to_f / max_len)
54
+ score += 100 * similarity
55
+ analysis[id_key_to_check.to_sym] = { similarity: similarity, expected: expected_attrs[id_key_to_check], actual: actual_id }
71
56
  end
72
- end
73
- nil
57
+
58
+ { score: score, name: element_on_screen[:name], attributes: element_on_screen[:attributes], analysis: analysis } if score > 75
59
+ end.compact
60
+
61
+ candidates.sort_by { |c| -c[:score] }.first
74
62
  end
75
63
 
76
- def self.find_similar_elements(failed_info, all_page_suggestions)
77
- failed_locator_value = (failed_info || {})[:selector_value]
78
- failed_locator_type = (failed_info || {})[:selector_type]
79
- return [] unless failed_locator_value && failed_locator_type
80
- normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
81
- cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
82
- similarities = []
83
- all_page_suggestions.each do |suggestion|
84
- candidate_locator = (suggestion[:locators] || []).find { |loc| loc[:strategy] == normalized_failed_type }
85
- next unless candidate_locator
86
- cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
87
- distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
88
- max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
89
- next if max_len.zero?
90
- similarity_score = 1.0 - (distance.to_f / max_len)
91
- if similarity_score > 0.85
92
- similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score, attributes: suggestion[:attributes] }
93
- end
64
+ private
65
+
66
+ def self.parse_locator(type, value, platform)
67
+ attrs = {}
68
+ if platform.to_s == 'ios'
69
+ attrs['name'] = value if type.to_s.include?('id')
70
+ else # Android
71
+ attrs['resource-id'] = value if type.to_s.include?('id')
72
+ end
73
+ if type.to_s == 'xpath'
74
+ value.scan(/@([\w\-]+)='([^']+)'/).each { |match| attrs[match[0]] = match[1] }
94
75
  end
95
- similarities.sort_by { |s| -s[:score] }.first(5)
76
+ attrs
96
77
  end
97
78
  end
98
79
  end
@@ -1,6 +1,7 @@
1
1
  # lib/appium_failure_helper/element_repository.rb
2
2
  module AppiumFailureHelper
3
3
  module ElementRepository
4
+ ELEMENTS = {}
4
5
  def self.load_all
5
6
  config = AppiumFailureHelper.configuration
6
7
  base_path = config.elements_path
@@ -38,48 +39,26 @@ module AppiumFailureHelper
38
39
  map
39
40
  end
40
41
 
41
- def self.load_all_from_yaml(base_path)
42
- elements_map = {}
43
- glob_path = File.join(base_path, '**', '*.yaml')
44
- files_found = Dir.glob(glob_path)
45
- files_found.each do |file|
46
- next if file.include?('reports_failure')
42
+ def self.load_all_from_yaml(dir_path = 'features/elements')
43
+ Dir.glob("#{dir_path}/*.yaml").each do |file|
47
44
  begin
48
- data = YAML.load_file(file)
49
- if data.is_a?(Hash)
50
- data.each do |k, v|
51
- data[k] = normalize_element(v)
52
- end
53
- elements_map.merge!(data)
45
+ yaml_data = YAML.load_file(file)
46
+ yaml_data.each do |key, value|
47
+ ELEMENTS[key.to_sym] = normalize_element(value)
54
48
  end
55
49
  rescue => e
56
50
  Utils.logger.warn("Aviso: Erro ao carregar o arquivo YAML #{file}: #{e.message}")
57
51
  end
58
52
  end
59
- elements_map
53
+ Utils.logger.info("Número de elementos carregados: #{ELEMENTS.size}")
54
+ ELEMENTS
60
55
  end
61
56
 
62
- def self.normalize_yaml_hash_keys(obj)
63
- case obj
64
- when Hash
65
- result = {}
66
- obj.each do |k, v|
67
- k_s = k.to_s
68
- v_n = normalize_yaml_hash_keys(v)
69
- # Se v_n é um Hash com chaves :value ou 'valor' -> faça unificação para 'value'
70
- if v_n.is_a?(Hash)
71
- if v_n.key?('valor') && !v_n.key?('value')
72
- v_n['value'] = v_n.delete('valor')
73
- end
74
- # também converte :tipoBusca para 'tipoBusca' (string)
75
- end
76
- result[k_s] = v_n
77
- end
78
- result
79
- when Array
80
- obj.map { |el| normalize_yaml_hash_keys(el) }
81
- else
82
- obj
57
+ def self.normalize_element(element_hash)
58
+ # Ajuste para garantir keys simbólicas e paths padrão
59
+ element_hash.transform_keys(&:to_sym).tap do |h|
60
+ h[:selector_type] ||= h[:type] || 'unknown'
61
+ h[:selector_value] ||= h[:value] || 'unknown'
83
62
  end
84
63
  end
85
64
  end
@@ -14,74 +14,158 @@ module AppiumFailureHelper
14
14
  def call
15
15
  begin
16
16
  unless @driver && @driver.session_id
17
- Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
18
17
  return
19
18
  end
20
19
 
21
20
  FileUtils.mkdir_p(@output_folder)
22
-
23
21
 
24
22
  triage_result = Analyzer.triage_error(@exception)
25
-
23
+ platform_value = @driver.capabilities[:platform_name] || @driver.capabilities['platformName']
24
+ platform = platform_value&.downcase || 'unknown'
25
+
26
26
  report_data = {
27
- exception: @exception,
28
- triage_result: triage_result,
29
- timestamp: @timestamp,
30
- platform: @driver.capabilities['platformName'] || @driver.capabilities[:platform_name] || 'unknown',
31
- screenshot_base64: @driver.screenshot_as(:base64)
27
+ exception: @exception, triage_result: triage_result,
28
+ timestamp: @timestamp, platform: platform,
29
+ screenshot_base_64: @driver.screenshot_as(:base64)
32
30
  }
33
31
 
34
32
  if triage_result == :locator_issue
35
- page_source = @driver.page_source rescue nil
33
+ page_source = @driver.page_source
36
34
  doc = Nokogiri::XML(page_source)
37
35
 
38
- # Inicializa failed_info primeiro
39
36
  failed_info = Analyzer.extract_failure_details(@exception) || {}
40
- failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {} if failed_info.empty?
41
- failed_info = { selector_type: 'unknown', selector_value: 'unknown' } if failed_info.empty?
42
-
43
- # Monta o report_data
44
- report_data[:page_source] = page_source
45
- report_data[:failed_element] = failed_info
37
+ if failed_info.empty?
38
+ failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
39
+ end
46
40
 
47
- # Analisa elementos similares, alternative xpaths, de_para, code search etc
48
- unless failed_info.empty?
49
- page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
41
+ if failed_info.empty?
42
+ report_data[:triage_result] = :unidentified_locator_issue
43
+ else
44
+ page_analyzer = PageAnalyzer.new(page_source, platform)
50
45
  all_page_elements = page_analyzer.analyze || []
51
- similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
46
+
47
+ best_candidate_analysis = Analyzer.perform_advanced_analysis(failed_info, all_page_elements, platform)
52
48
 
53
49
  alternative_xpaths = []
54
- if !similar_elements.empty?
55
- target_suggestion = similar_elements.first
56
- if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
50
+ if best_candidate_analysis
51
+ if best_candidate_analysis[:attributes] && (target_path = best_candidate_analysis[:attributes][:path])
57
52
  target_node = doc.at_xpath(target_path)
58
- alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
53
+ if target_node
54
+ alternative_xpaths = XPathFactory.generate_for_node(target_node)
55
+ end
59
56
  end
60
57
  end
61
58
 
62
- unified_element_map = ElementRepository.load_all
63
- de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
64
- code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
65
-
66
- report_data.merge!(
67
- similar_elements: similar_elements,
68
- alternative_xpaths: alternative_xpaths,
69
- de_para_analysis: de_para_result,
70
- code_search_results: code_search_results,
71
- all_page_elements: all_page_elements
72
- )
59
+ report_data.merge!({
60
+ page_source: page_source,
61
+ failed_element: failed_info,
62
+ best_candidate_analysis: best_candidate_analysis,
63
+ alternative_xpaths: alternative_xpaths,
64
+ all_page_elements: all_page_elements
65
+ })
73
66
  end
67
+ end
74
68
 
75
- # Gera o relatório
76
- ReportGenerator.new(@output_folder, report_data).generate_all
77
- Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
69
+ ReportGenerator.new(@output_folder, report_data).generate_all
70
+ Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
71
+ rescue => e
72
+ puts "--- ERRO FATAL NA GEM ---"
73
+ puts "CLASSE: #{e.class}, MENSAGEM: #{e.message}"
74
+ puts e.backtrace.join("\n")
75
+ puts "-------------------------"
76
+ end
77
+ report_data
78
+ end
78
79
 
79
- report_data
80
- return
80
+ private
81
+
82
+ def fetch_failed_element
83
+ msg = @exception&.message.to_s
84
+
85
+ if (m = msg.match(/using\s+["']?([^"']+)["']?\s+with\s+value\s+["']([^"']+)["']/i))
86
+ return { selector_type: m[1], selector_value: m[2] }
87
+ end
88
+
89
+ if (m = msg.match(/"method"\s*:\s*"([^"]+)"[\s,}].*"selector"\s*:\s*"([^"]+)"/i))
90
+ return { selector_type: m[1], selector_value: m[2] }
91
+ end
92
+
93
+ if (m = msg.match(/["']([^"']+)["']/))
94
+ maybe_value = m[1]
95
+ unified_map = ElementRepository.load_all rescue {}
96
+ found = find_in_element_repository_by_value(maybe_value, unified_map)
97
+ if found
98
+ return found
99
+ end
100
+
101
+ guessed_type = msg[/\b(xpath|id|accessibility id|css)\b/i] ? $&.downcase : nil
102
+ return { selector_type: guessed_type || 'unknown', selector_value: maybe_value }
103
+ end
104
+
105
+ begin
106
+ code_info = SourceCodeAnalyzer.extract_from_exception(@exception) rescue {}
107
+ unless code_info.nil? || code_info.empty?
108
+ return code_info
109
+ end
110
+ rescue => _; end
111
+
112
+ unified_map = ElementRepository.load_all rescue {}
113
+ unified_map.each do |k, v|
114
+ k_str = k.to_s.downcase
115
+ if msg.downcase.include?(k_str)
116
+ return normalize_repo_element(v)
117
+ end
118
+ vals = []
119
+ if v.is_a?(Hash)
120
+ vals << v['valor'] if v.key?('valor')
121
+ vals << v['value'] if v.key?('value')
122
+ vals << v[:valor] if v.key?(:valor)
123
+ vals << v[:value] if v.key?(:value)
124
+ end
125
+ vals.compact!
126
+ vals.each do |vv|
127
+ if vv.to_s.downcase == vv.to_s.downcase && msg.downcase.include?(vv.to_s.downcase)
128
+ return normalize_repo_element(v)
129
+ end
130
+ end
131
+ end
132
+
133
+ # final fallback
134
+ debug_log("fetch_failed_element: fallback unknown")
135
+ { selector_type: 'unknown', selector_value: 'unknown' }
136
+ end
137
+
138
+ def find_in_element_repository_by_value(value, map = {})
139
+ return nil if value.nil? || value.to_s.strip.empty?
140
+ normalized_value = value.to_s.downcase.strip
141
+ map.each do |k, v|
142
+ entry = v.is_a?(Hash) ? v : (v.respond_to?(:to_h) ? v.to_h : nil)
143
+ next unless entry
144
+ entry_val = entry['valor'] || entry['value'] || entry[:valor] || entry[:value] || entry['locator'] || entry[:locator]
145
+ next unless entry_val
146
+ return normalize_repo_element(entry) if entry_val.to_s.downcase.strip == normalized_value
147
+ end
148
+ nil
149
+ end
150
+
151
+ def normalize_repo_element(entry)
152
+ return nil unless entry.is_a?(Hash)
153
+ tipo = entry['tipoBusca'] || entry[:tipoBusca] || entry['type'] || entry[:type] || entry['search_type'] || entry[:search]
154
+ valor = entry['valor'] || entry[:value] || entry[:locator] || entry[:valor_final] || entry[:value_final]
155
+ return nil unless valor
156
+ { selector_type: (tipo || 'unknown'), selector_value: valor.to_s }
157
+ end
158
+
159
+ def generate_alternative_xpaths(similar_elements, doc)
160
+ alternative_xpaths = []
161
+ if !similar_elements.empty?
162
+ target_suggestion = similar_elements.first
163
+ if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
164
+ target_node = doc.at_xpath(target_path) rescue nil
165
+ alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
81
166
  end
82
- rescue => e
83
- Utils.logger.error("Erro fatal na GEM de diagnóstico: #{e.message}\n#{e.backtrace.join("\n")}")
84
167
  end
168
+ alternative_xpaths
85
169
  end
86
170
  end
87
- end
171
+ end
@@ -20,28 +20,20 @@ module AppiumFailureHelper
20
20
  @platform = platform
21
21
  end
22
22
 
23
- def analyze
24
- seen_elements = {}
23
+ def analyze
25
24
  all_elements_suggestions = []
26
25
  @doc.xpath('//*').each do |node|
27
- next if ['hierarchy', 'AppiumAUT'].include?(node.name)
28
- attrs = node.attributes.transform_values(&:value)
29
-
30
- unique_key = node.path
31
- next if seen_elements[unique_key]
26
+ next if ['hierarchy', 'AppiumAUT'].include?(node.name)
32
27
 
33
- name = suggest_name(node.name, attrs)
34
-
35
- locators = XPathFactory.generate_for_node(node)
36
-
37
- all_elements_suggestions << {
38
- name: name,
39
- locators: locators,
40
- attributes: attrs.merge(tag: node.name, path: node.path)
41
- }
42
- seen_elements[unique_key] = true
28
+ attrs = node.attribute_nodes.to_h { |attr| [attr.name, attr.value] }
29
+
30
+ attrs['tag'] = node.name
31
+ name = suggest_name(node.name, attrs)
32
+ locators = XPathFactory.generate_for_node(node)
33
+
34
+ all_elements_suggestions << { name: name, locators: locators, attributes: attrs.merge(path: node.path) }
43
35
  end
44
- all_elements_suggestions
36
+ all_elements_suggestions.uniq { |s| s[:attributes][:path] }
45
37
  end
46
38
 
47
39
  private
@@ -3,7 +3,7 @@ module AppiumFailureHelper
3
3
  def initialize(output_folder, report_data)
4
4
  @output_folder = output_folder
5
5
  @data = report_data
6
- @page_source = report_data[:page_source] # Pega o page_source de dentro do hash
6
+ @page_source = report_data[:page_source]
7
7
  end
8
8
 
9
9
  def generate_all
@@ -18,7 +18,7 @@ module AppiumFailureHelper
18
18
  File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
19
19
  end
20
20
 
21
- def generate_yaml_reports
21
+ def generate_yaml_reports
22
22
  analysis_report = {
23
23
  triage_result: @data[:triage_result],
24
24
  exception_class: @data[:exception].class.to_s,
@@ -27,86 +27,97 @@ module AppiumFailureHelper
27
27
  best_candidate_analysis: @data[:best_candidate_analysis]
28
28
  }
29
29
  File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
30
-
30
+
31
31
  if @data[:all_page_elements]
32
32
  File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
33
33
  end
34
34
  end
35
35
 
36
36
  def generate_html_report
37
- html_content = case @data[:triage_result]
38
- when :locator_issue
37
+ html_content = if @data[:triage_result] == :locator_issue && !(@data[:failed_element] || {}).empty?
39
38
  build_full_report
40
39
  else
41
40
  build_simple_diagnosis_report(
42
41
  title: "Diagnóstico Rápido de Falha",
43
- message: "A falha não foi causada por um seletor não encontrado ou a análise do seletor falhou. Verifique a mensagem de erro original e o stack trace para a causa raiz."
42
+ message: "A análise profunda do seletor não foi executada ou falhou. Verifique a mensagem de erro original e o stack trace."
44
43
  )
45
44
  end
46
-
47
45
  File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
48
46
  end
49
47
 
50
48
  def build_full_report
51
49
  failed_info = @data[:failed_element] || {}
52
- similar_elements = @data[:similar_elements] || []
53
50
  all_suggestions = @data[:all_page_elements] || []
54
- de_para_analysis = @data[:de_para_analysis]
55
- code_search_results = @data[:code_search_results] || []
51
+ best_candidate = @data[:best_candidate_analysis]
56
52
  alternative_xpaths = @data[:alternative_xpaths] || []
57
53
  timestamp = @data[:timestamp]
58
54
  platform = @data[:platform]
59
- screenshot_base64 = @data[:screenshot_base64]
55
+ screenshot_base64 = @data[:screenshot_base_64]
60
56
 
61
57
  locators_html = lambda do |locators|
62
- (locators || []).map do |loc|
63
- strategy_text = loc[:strategy].to_s.upcase.gsub('_', ' ')
64
- "<li class='flex justify-between items-center bg-gray-50 p-2 rounded-md mb-1 text-xs font-mono'><span class='font-bold text-indigo-600'>#{CGI.escapeHTML(strategy_text)}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>"
65
- end.join
58
+ (locators || []).map { |loc| "<li class='flex justify-between items-center bg-gray-50 p-2 rounded-md mb-1 text-xs font-mono'><span class='font-bold text-indigo-600'>#{CGI.escapeHTML(loc[:strategy].to_s.upcase.gsub('_', ' '))}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>" }.join
66
59
  end
67
60
 
68
61
  all_elements_html = lambda do |elements|
69
62
  (elements || []).map { |el| "<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{CGI.escapeHTML(el[:name])}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>" }.join
70
63
  end
71
-
72
- failed_info_content = "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
73
64
 
74
- code_search_html = "" # (Sua lógica code_search_html)
75
- failed_info_content = if failed_info && !failed_info.empty?
76
- "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
77
- else
78
- "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
79
- end
65
+ failed_info_content = "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-words'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
80
66
 
81
- code_search_html = ""
82
- unless code_search_results.empty?
83
- suggestions_list = code_search_results.map do |match|
84
- score_percent = (match[:score] * 100).round(1)
85
- "<div class='border border-sky-200 bg-sky-50 p-3 rounded-lg mb-2'><p class='text-sm text-gray-600'>Encontrado em: <strong class='font-mono'>#{match[:file]}:#{match[:line_number]}</strong></p><pre class='bg-gray-800 text-white p-2 rounded mt-2 text-xs overflow-auto'><code>#{CGI.escapeHTML(match[:code])}</code></pre><p class='text-xs text-green-600 mt-1'>Similaridade: #{score_percent}%</p></div>"
86
- end.join
87
- code_search_html = "<div class='bg-white p-4 rounded-lg shadow-md'><h2 class='text-xl font-bold text-sky-700 mb-4'>Sugestões Encontradas no Código</h2>#{suggestions_list}</div>"
88
- end
67
+ advanced_analysis_html = if best_candidate.nil?
68
+ "<p class='text-gray-500'>Nenhum candidato provável foi encontrado na tela atual para uma análise detalhada.</p>"
69
+ else
70
+ analysis_details = (best_candidate[:analysis] || {}).map do |key, data|
71
+ status_color = 'bg-gray-400'
72
+ status_icon = '⚪'
73
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Não verificado</span>"
89
74
 
90
- # --- LÓGICA RESTAURADA: ELEMENTO COM FALHA ---
91
- failed_info_content = if failed_info && !failed_info.empty?
92
- "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
93
- else
94
- "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
95
- end
75
+ if data[:match] == true || (data[:similarity] && data[:similarity] == 1.0)
76
+ status_color = 'bg-green-500'
77
+ status_icon = ''
78
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Correspondência Exata!</span>"
79
+ elsif data[:similarity] && data[:similarity] > 0.7
80
+ status_color = 'bg-yellow-500'
81
+ status_icon = '⚠️'
82
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Parecido (Encontrado: '#{CGI.escapeHTML(data[:actual])}')</span>"
83
+ else
84
+ status_color = 'bg-red-500'
85
+ status_icon = '❌'
86
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Diferente! Esperado: '#{CGI.escapeHTML(data[:expected].to_s)}'</span>"
87
+ end
88
+
89
+ "<li class='flex items-center text-sm'><span class='w-4 h-4 rounded-full #{status_color} mr-3 flex-shrink-0 flex items-center justify-center text-white text-xs'>#{status_icon}</span><div class='truncate'>#{status_text}</div></li>"
90
+ end.join
91
+
92
+ suggestion_text = "O `resource-id` pode ter mudado ou o `text` está diferente. Considere usar um seletor mais robusto baseado nos atributos que corresponderam."
93
+ if (best_candidate[:analysis][:id] || {})[:match] == true && (best_candidate[:analysis][:text] || {})[:similarity].to_f < 0.7
94
+ suggestion_text = "O `resource-id` corresponde, mas o texto é diferente. **Recomendamos fortemente usar o `resource-id` para este seletor.**"
95
+ end
96
96
 
97
- repair_suggestions_content = if alternative_xpaths.empty?
98
- "<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>"
99
- else
100
- pages = alternative_xpaths.each_slice(6).to_a
101
- carousel_items = pages.map do |page_strategies|
102
- strategy_list_html = page_strategies.map do |strategy|
103
- reliability_color = case strategy[:reliability]
104
- when :alta then 'bg-green-100 text-green-800'
105
- when :media then 'bg-yellow-100 text-yellow-800'
106
- else 'bg-red-100 text-red-800'
107
- end
108
- # CORREÇÃO: Adiciona o tipo de estratégia (ID, XPATH) ao lado do seletor
109
- <<~STRATEGY_ITEM
97
+ <<~HTML
98
+ <div class='border border-sky-200 bg-sky-50 p-4 rounded-lg'>
99
+ <h4 class='font-bold text-sky-800 mb-3'>Candidato Mais Provável Encontrado: <span class='font-mono bg-sky-100 text-sky-900 rounded px-2 py-1 text-sm'>#{CGI.escapeHTML(best_candidate[:name])}</span></h4>
100
+ <ul class='space-y-2 mb-4'>#{analysis_details}</ul>
101
+ <div class='bg-sky-100 border-l-4 border-sky-500 text-sky-900 text-sm p-3 rounded-r-lg'>
102
+ <p><b>Sugestão:</b> #{suggestion_text}</p>
103
+ </div>
104
+ </div>
105
+ HTML
106
+ end
107
+
108
+ repair_strategies_content = if alternative_xpaths.empty?
109
+ "<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>"
110
+ else
111
+ pages = alternative_xpaths.each_slice(6).to_a
112
+ carousel_items = pages.map do |page_strategies|
113
+ strategy_list_html = page_strategies.map do |strategy|
114
+ reliability_color = case strategy[:reliability]
115
+ when :alta then 'bg-green-100 text-green-800'
116
+ when :media then 'bg-yellow-100 text-yellow-800'
117
+ else 'bg-red-100 text-red-800'
118
+ end
119
+ # CORREÇÃO: Adiciona o tipo de estratégia (ID, XPATH) ao lado do seletor
120
+ <<~STRATEGY_ITEM
110
121
  <li class='border-b border-gray-200 py-3 last:border-b-0'>
111
122
  <div class='flex justify-between items-center mb-1'>
112
123
  <p class='font-semibold text-indigo-800 text-sm'>#{CGI.escapeHTML(strategy[:name])}</p>
@@ -118,11 +129,11 @@ module AppiumFailureHelper
118
129
  </div>
119
130
  </li>
120
131
  STRATEGY_ITEM
121
- end.join
122
- "<div class='carousel-item w-full flex-shrink-0'><ul>#{strategy_list_html}</ul></div>"
123
- end.join
132
+ end.join
133
+ "<div class='carousel-item w-full flex-shrink-0'><ul>#{strategy_list_html}</ul></div>"
134
+ end.join
124
135
 
125
- <<~CAROUSEL
136
+ <<~CAROUSEL
126
137
  <div id="xpath-carousel" class="relative">
127
138
  <div class="overflow-hidden">
128
139
  <div class="carousel-track flex transition-transform duration-300 ease-in-out">
@@ -140,34 +151,7 @@ module AppiumFailureHelper
140
151
  </div>
141
152
  </div>
142
153
  CAROUSEL
143
- end
144
-
145
- similar_elements_content = if similar_elements.empty?
146
- "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>"
147
- else
148
- carousel_items = similar_elements.map do |el|
149
- score_percent = (el[:score] * 100).round(1)
150
- <<~ITEM
151
- <div class="carousel-item w-full flex-shrink-0">
152
- <div class='border border-indigo-100 p-4 rounded-lg bg-indigo-50'>
153
- <p class='font-bold text-indigo-800 mb-2'>#{CGI.escapeHTML(el[:name])} <span class='text-xs font-normal text-green-600 bg-green-100 rounded-full px-2 py-1 ml-2'>Similaridade: #{score_percent}%</span></p>
154
- <ul>#{locators_html.call(el[:locators])}</ul>
155
- </div>
156
- </div>
157
- ITEM
158
- end.join
159
- <<~CAROUSEL
160
- <div id="similar-elements-carousel" class="relative">
161
- <div class="overflow-hidden rounded-lg bg-white"><div class="carousel-track flex transition-transform duration-300 ease-in-out">#{carousel_items}</div></div>
162
- <div class="flex items-center justify-center space-x-4 mt-4">
163
- <button class="carousel-prev-footer bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg disabled:opacity-50"> &lt; Anterior </button>
164
- <div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
165
- <button class="carousel-next-footer bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg disabled:opacity-50"> Próximo &gt; </button>
166
- </div>
167
- </div>
168
- CAROUSEL
169
- end
170
-
154
+ end
171
155
  <<~HTML_REPORT
172
156
  <!DOCTYPE html>
173
157
  <html lang="pt-BR">
@@ -189,7 +173,6 @@ module AppiumFailureHelper
189
173
  <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
190
174
  #{failed_info_content}
191
175
  </div>
192
- #{code_search_html}
193
176
  <div class="bg-white p-4 rounded-lg shadow-md">
194
177
  <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
195
178
  <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
@@ -198,13 +181,14 @@ module AppiumFailureHelper
198
181
  <div class="lg:col-span-2">
199
182
  <div class="bg-white rounded-lg shadow-md">
200
183
  <div class="flex border-b border-gray-200">
201
- <button class="tab-button active px-4 py-3 text-sm" data-tab="strategies">Estratégias de Reparo (#{alternative_xpaths.size})</button>
184
+ <button class="tab-button active px-4 py-3 text-sm" data-tab="analysis">Análise Avançada</button>
202
185
  <button class="tab-button px-4 py-3 text-sm text-gray-600" data-tab="all">Dump Completo (#{all_suggestions.size})</button>
203
186
  </div>
204
187
  <div class="p-6">
205
- <div id="strategies" class="tab-content active">
206
- <h3 class="text-lg font-semibold text-indigo-700 mb-4">Estratégias de Localização Alternativas</h3>
207
- #{repair_suggestions_content}
188
+ <div id="analysis" class="tab-content active">
189
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Diagnóstico por Atributos Ponderados</h3>
190
+ #{advanced_analysis_html}
191
+ #{repair_strategies_content}
208
192
  </div>
209
193
  <div id="all" class="tab-content">
210
194
  <h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
@@ -215,7 +199,7 @@ module AppiumFailureHelper
215
199
  </div>
216
200
  </div>
217
201
  </div>
218
- <script>
202
+ <script>
219
203
  document.addEventListener('DOMContentLoaded', () => {
220
204
  const tabs = document.querySelectorAll('.tab-button');
221
205
  tabs.forEach(tab => {
@@ -228,43 +212,56 @@ module AppiumFailureHelper
228
212
  document.getElementById(target).classList.add('active');
229
213
  });
230
214
  });
231
-
232
- const carousel = document.getElementById('xpath-carousel');
233
- if (carousel) {
234
- const track = carousel.querySelector('.carousel-track');
235
- const items = carousel.querySelectorAll('.carousel-item');
236
- const prevButton = carousel.querySelector('.carousel-prev-footer');
237
- const nextButton = carousel.querySelector('.carousel-next-footer');
238
- const counter = carousel.querySelector('.carousel-counter');
239
- const totalItems = items.length;
240
- let currentIndex = 0;
241
-
242
- function updateCarousel() {
243
- if (totalItems === 0) {
244
- if(counter) counter.textContent = "";
245
- return;
246
- };
247
- track.style.transform = `translateX(-${currentIndex * 100}%)`;
248
- if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
249
- if (prevButton) { prevButton.disabled = currentIndex === 0; }
250
- if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
251
- }
252
-
253
- if (nextButton) {
254
- nextButton.addEventListener('click', () => {
255
- if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
256
- });
257
- }
258
-
259
- if (prevButton) {
260
- prevButton.addEventListener('click', () => {
261
- if (currentIndex > 0) { currentIndex--; updateCarousel(); }
262
- });
263
- }
264
-
265
- if (totalItems > 0) { updateCarousel(); }
266
- }
267
215
  });
216
+ document.addEventListener('DOMContentLoaded', () => {
217
+ const tabs = document.querySelectorAll('.tab-button');
218
+ tabs.forEach(tab => {
219
+ tab.addEventListener('click', (e) => {
220
+ e.preventDefault();
221
+ const target = tab.getAttribute('data-tab');
222
+ tabs.forEach(t => t.classList.remove('active'));
223
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
224
+ tab.classList.add('active');
225
+ document.getElementById(target).classList.add('active');
226
+ });
227
+ });
228
+
229
+ const carousel = document.getElementById('xpath-carousel');
230
+ if (carousel) {
231
+ const track = carousel.querySelector('.carousel-track');
232
+ const items = carousel.querySelectorAll('.carousel-item');
233
+ const prevButton = carousel.querySelector('.carousel-prev-footer');
234
+ const nextButton = carousel.querySelector('.carousel-next-footer');
235
+ const counter = carousel.querySelector('.carousel-counter');
236
+ const totalItems = items.length;
237
+ let currentIndex = 0;
238
+
239
+ function updateCarousel() {
240
+ if (totalItems === 0) {
241
+ if(counter) counter.textContent = "";
242
+ return;
243
+ };
244
+ track.style.transform = `translateX(-${currentIndex * 100}%)`;
245
+ if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
246
+ if (prevButton) { prevButton.disabled = currentIndex === 0; }
247
+ if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
248
+ }
249
+
250
+ if (nextButton) {
251
+ nextButton.addEventListener('click', () => {
252
+ if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
253
+ });
254
+ }
255
+
256
+ if (prevButton) {
257
+ prevButton.addEventListener('click', () => {
258
+ if (currentIndex > 0) { currentIndex--; updateCarousel(); }
259
+ });
260
+ }
261
+
262
+ if (totalItems > 0) { updateCarousel(); }
263
+ }
264
+ });
268
265
  </script>
269
266
  </body>
270
267
  </html>
@@ -273,6 +270,7 @@ module AppiumFailureHelper
273
270
 
274
271
  def build_simple_diagnosis_report(title:, message:)
275
272
  exception = @data[:exception]
273
+ screenshot = @data[:screenshot_base_64]
276
274
  error_message_html = CGI.escapeHTML(exception.message.to_s)
277
275
  backtrace_html = CGI.escapeHTML(exception.backtrace.join("\n"))
278
276
 
@@ -294,7 +292,7 @@ module AppiumFailureHelper
294
292
  <div class="md:col-span-1">
295
293
  <div class="bg-white p-4 rounded-lg shadow-md">
296
294
  <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
297
- <img src="data:image/png;base64,#{@data[:screenshot_base64]}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
295
+ <img src="data:image/png;base64,#{screenshot}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
298
296
  </div>
299
297
  </div>
300
298
  <div class="md:col-span-2 space-y-6">
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.1"
3
3
  end
data/release_gem.rb CHANGED
@@ -60,6 +60,7 @@ def git_commit_and_tag(new_version)
60
60
  `git add .`
61
61
  `git commit -m "Bump version to #{new_version.join('.')}"`
62
62
  `git tag v#{new_version.join('.')}`
63
+ `git push && git push --tags`
63
64
  end
64
65
 
65
66
  # Publicar a GEM
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appium_failure_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-02 00:00:00.000000000 Z
11
+ date: 2025-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -90,6 +90,11 @@ executables: []
90
90
  extensions: []
91
91
  extra_rdoc_files: []
92
92
  files:
93
+ - ".idea/.gitignore"
94
+ - ".idea/Appium_failure_helper.iml"
95
+ - ".idea/misc.xml"
96
+ - ".idea/modules.xml"
97
+ - ".idea/vcs.xml"
93
98
  - ".rspec"
94
99
  - LICENSE.txt
95
100
  - README.md