appium_failure_helper 0.6.0 → 0.6.2

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: cb96de86c564a4b997d4d1a4320e831e3d4f28a3f8d7e97623b347d855588303
4
- data.tar.gz: 2462b34e6f3c01ea63a8a68d9c1814fd4138dc2c0dbcdbef0aecb8d6ba82fece
3
+ metadata.gz: 87d48b6025e058dfc945cc71675a8389ffd7c219a8bedccf8994327b10d02760
4
+ data.tar.gz: 700ae3934bd4413d7ea30ac589493dca717e199dd0a69374156ec2f4bda966b3
5
5
  SHA512:
6
- metadata.gz: ac755fa2ec04c3038c27c9ded178be9b6499de3c7a9cc70c96a5dfebd2fd07d6304eb236720cc4e4dfb1f31807b731c50512973af875596925eef72a9290f1b0
7
- data.tar.gz: 410501dec6ffa017effc75796cafb04b83a371bcd6f4b5b466b60f70339193b64f743112870e88cade771ed0ef70f4b0626fd4a5fc845173225d7cb4952052a5
6
+ metadata.gz: 83321c33adbc356d531abe28f2f792b635e5e2658676c7510afabd263e4c7f99b4d4c5dcbaaf0d20bba569a62841b812f50015b5901f3e860ebe0b6f44bbbcc2
7
+ data.tar.gz: 35935a71ebd0ff920cfeee986d0918337f706146b680c7a5f85bba334dcc790d0ca4c01bb8bcf1b3aec2efaa95a5d758fc6b416ece69bd2504b3c9b59ac45a91
@@ -3,188 +3,165 @@ require 'fileutils'
3
3
  require 'base64'
4
4
  require 'yaml'
5
5
  require 'logger'
6
+ require 'did_you_mean'
7
+ require 'cgi' # Adicionado para garantir o escape de HTML
6
8
 
7
9
  module AppiumFailureHelper
8
10
  class Capture
11
+ # ... (Constantes PREFIX, MAX_VALUE_LENGTH, @@logger permanecem iguais) ...
9
12
  PREFIX = {
10
- 'android.widget.Button' => 'btn',
11
- 'android.widget.TextView' => 'txt',
12
- 'android.widget.ImageView' => 'img',
13
- 'android.widget.EditText' => 'input',
14
- 'android.widget.CheckBox' => 'chk',
15
- 'android.widget.RadioButton' => 'radio',
16
- 'android.widget.Switch' => 'switch',
17
- 'android.widget.ViewGroup' => 'group',
18
- 'android.widget.View' => 'view',
19
- 'android.widget.FrameLayout' => 'frame',
20
- 'android.widget.LinearLayout' => 'linear',
21
- 'android.widget.RelativeLayout' => 'relative',
22
- 'android.widget.ScrollView' => 'scroll',
23
- 'android.webkit.WebView' => 'web',
24
- 'android.widget.Spinner' => 'spin',
25
- 'XCUIElementTypeButton' => 'btn',
26
- 'XCUIElementTypeStaticText' => 'txt',
27
- 'XCUIElementTypeTextField' => 'input',
28
- 'XCUIElementTypeImage' => 'img',
29
- 'XCUIElementTypeSwitch' => 'switch',
30
- 'XCUIElementTypeScrollView' => 'scroll',
31
- 'XCUIElementTypeOther' => 'elm',
32
- 'XCUIElementTypeCell' => 'cell',
13
+ 'android.widget.Button' => 'btn', 'android.widget.TextView' => 'txt',
14
+ 'android.widget.ImageView' => 'img', 'android.widget.EditText' => 'input',
15
+ 'android.widget.CheckBox' => 'chk', 'android.widget.RadioButton' => 'radio',
16
+ 'android.widget.Switch' => 'switch', 'android.widget.ViewGroup' => 'group',
17
+ 'android.widget.View' => 'view', 'android.widget.FrameLayout' => 'frame',
18
+ 'android.widget.LinearLayout' => 'linear', 'android.widget.RelativeLayout' => 'relative',
19
+ 'android.widget.ScrollView' => 'scroll', 'android.webkit.WebView' => 'web',
20
+ 'android.widget.Spinner' => 'spin', 'XCUIElementTypeButton' => 'btn',
21
+ 'XCUIElementTypeStaticText' => 'txt', 'XCUIElementTypeTextField' => 'input',
22
+ 'XCUIElementTypeImage' => 'img', 'XCUIElementTypeSwitch' => 'switch',
23
+ 'XCUIElementTypeScrollView' => 'scroll', 'XCUIElementTypeOther' => 'elm',
24
+ 'XCUIElementTypeCell' => 'cell'
33
25
  }.freeze
34
-
35
26
  MAX_VALUE_LENGTH = 100
36
27
  @@logger = nil
37
28
 
29
+ # --- MÉTODO PRINCIPAL (SEM ALTERAÇÕES) ---
38
30
  def self.handler_failure(driver, exception)
39
31
  begin
40
32
  self.setup_logger unless @@logger
41
-
42
- # Remove a pasta reports_failure ao iniciar uma nova execução
43
- FileUtils.rm_rf("reports_failure")
44
- @@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
45
-
46
33
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
47
34
  output_folder = "reports_failure/failure_#{timestamp}"
48
-
49
35
  FileUtils.mkdir_p(output_folder)
50
36
  @@logger.info("Pasta de saída criada: #{output_folder}")
51
-
52
- # Captura o Base64 e salva o PNG
53
37
  screenshot_base64 = driver.screenshot_as(:base64)
54
- screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
55
- File.open(screenshot_path, 'wb') do |f|
56
- f.write(Base64.decode64(screenshot_base64))
57
- end
58
- @@logger.info("Screenshot salvo em #{screenshot_path}")
59
-
60
38
  page_source = driver.page_source
61
- xml_path = "#{output_folder}/page_source_#{timestamp}.xml"
62
- File.write(xml_path, page_source)
63
- @@logger.info("Page source salvo em #{xml_path}")
64
-
39
+ File.write("#{output_folder}/page_source_#{timestamp}.xml", page_source)
65
40
  doc = Nokogiri::XML(page_source)
66
41
  platform = driver.capabilities['platformName']&.downcase || 'unknown'
67
-
68
42
  failed_element_info = self.extract_info_from_exception(exception)
69
-
70
- # --- Processamento de todos os elementos ---
71
- seen_elements = {}
72
- all_elements_suggestions = []
73
- doc.xpath('//*').each do |node|
74
- next if node.name == 'hierarchy'
75
- attrs = node.attributes.transform_values(&:value)
76
-
77
- unique_key = "#{node.name}|#{attrs['resource-id'].to_s}|#{attrs['content-desc'].to_s}|#{attrs['text'].to_s}"
78
-
79
- unless seen_elements[unique_key]
80
- name = self.suggest_name(node.name, attrs)
81
- locators = self.xpath_generator(node.name, attrs, platform)
82
-
83
- all_elements_suggestions << { name: name, locators: locators }
84
- seen_elements[unique_key] = true
85
- end
43
+ local_element_map = self.load_local_element_map
44
+ de_para_result = nil
45
+ logical_name_key = failed_element_info[:selector_value].to_s.gsub(/^#/, '')
46
+ if local_element_map.key?(logical_name_key)
47
+ de_para_result = {
48
+ logical_name: logical_name_key,
49
+ correct_locator: local_element_map[logical_name_key]
50
+ }
86
51
  end
87
-
88
- # --- Geração do Relatório FOCADO (1) ---
52
+ all_elements_suggestions = self.get_all_elements_from_screen(doc, platform)
53
+ similar_elements = self.find_similar_elements(failed_element_info, all_elements_suggestions)
89
54
  targeted_report = {
90
55
  failed_element: failed_element_info,
91
- similar_elements: [],
56
+ similar_elements: similar_elements,
57
+ de_para_analysis: de_para_result
92
58
  }
93
-
94
- if failed_element_info && failed_element_info[:selector_value]
95
- targeted_report[:similar_elements] = self.find_similar_elements(doc, failed_element_info, platform)
96
- end
97
-
98
- targeted_yaml_path = "#{output_folder}/failure_analysis_#{timestamp}.yaml"
99
- File.open(targeted_yaml_path, 'w') do |f|
100
- f.write(YAML.dump(targeted_report))
101
- end
102
- @@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
103
-
104
- # --- Geração do Relatório COMPLETO (2) ---
105
- full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
106
- File.open(full_dump_yaml_path, 'w') do |f|
107
- f.write(YAML.dump(all_elements_suggestions))
108
- end
109
- @@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
110
-
111
- # --- Geração do Relatório HTML (3) ---
112
- html_report_path = "#{output_folder}/report_#{timestamp}.html"
59
+ File.open("#{output_folder}/failure_analysis_#{timestamp}.yaml", 'w') { |f| f.write(YAML.dump(targeted_report)) }
60
+ File.open("#{output_folder}/all_elements_dump_#{timestamp}.yaml", 'w') { |f| f.write(YAML.dump(all_elements_suggestions)) }
113
61
  html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
114
- File.write(html_report_path, html_content)
115
- @@logger.info("Relatório HTML completo salvo em #{html_report_path}")
116
-
62
+ File.write("#{output_folder}/report_#{timestamp}.html", html_content)
63
+ @@logger.info("Relatórios gerados com sucesso em: #{output_folder}")
117
64
  rescue => e
118
65
  @@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
119
66
  end
120
67
  end
121
68
 
122
69
  private
123
-
124
- def self.setup_logger
125
- @@logger = Logger.new(STDOUT)
126
- @@logger.level = Logger::INFO
127
- @@logger.formatter = proc do |severity, datetime, progname, msg|
128
- "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
70
+
71
+ def self.load_local_element_map
72
+ elements_map = {}
73
+ glob_path = File.join(Dir.pwd, 'features', 'elements', '**', '*.yaml')
74
+ Dir.glob(glob_path).each do |file|
75
+ begin
76
+ data = YAML.load_file(file)
77
+ if data.is_a?(Hash)
78
+ data.each do |key, value|
79
+ if value.is_a?(Hash) && value['tipoBusca'] && value['valor']
80
+ elements_map[key] = value
81
+ end
82
+ end
83
+ end
84
+ rescue => e
85
+ @@logger.warn("Aviso: Erro ao carregar o arquivo de elementos #{file}: #{e.message}") if @@logger
86
+ end
129
87
  end
88
+ elements_map
130
89
  end
131
-
90
+
91
+ def self.setup_logger
92
+ @@logger = Logger.new(STDOUT)
93
+ @@logger.level = Logger::INFO
94
+ @@logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n" }
95
+ end
96
+
132
97
  def self.extract_info_from_exception(exception)
133
- message = exception.message
134
- info = {}
135
-
136
- # Corrigido: Usando múltiplos padrões para extração robusta (resolvendo o problema do YAML vazio)
137
- patterns = [
138
- /(?:could not be found|cannot find element) using (.+)=['"]?(.+)['"]?/i,
139
- /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
140
- /(?:An element with the selector |element with the selector |selector |element with the |element identified by )(.+?) (?:could not be found|was not found|not found|not be located)/i,
141
- /(?:with the resource-id|with the accessibility-id) ['"](.+?)['"]/i
142
- ]
143
-
144
- patterns.each do |pattern|
145
- match = message.match(pattern)
146
- if match
147
- selector_value = match.captures.last.strip
148
- selector_type = match[1]&.strip || 'Unknown'
149
-
150
- info[:selector_type] = selector_type
151
- info[:selector_value] = selector_value.gsub(/['"]/, '')
152
- return info
98
+ message = exception.message
99
+ info = {}
100
+ patterns = [
101
+ /element with locator ['"]?(#?\w+)['"]?/i,
102
+ /(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
103
+ /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
104
+ /(?:with the resource-id|with the accessibility-id) ['"]?(.+?)['"]?/i
105
+ ]
106
+ patterns.each do |pattern|
107
+ match = message.match(pattern)
108
+ if match
109
+ info[:selector_value] = match.captures.last.strip.gsub(/['"]/, '')
110
+ info[:selector_type] = match.captures.size > 1 ? match.captures[0].strip.gsub(/['"]/, '') : 'id' # Padroniza para 'id'
111
+ return info
112
+ end
153
113
  end
154
- end
155
- info
114
+ info
156
115
  end
157
-
158
- def self.find_similar_elements(doc, failed_info, platform)
159
- similar_elements = []
160
- doc.xpath('//*').each do |node|
161
- next if node.name == 'hierarchy'
162
- attrs = node.attributes.transform_values(&:value)
116
+
117
+
118
+ def self.find_similar_elements(failed_element_info, all_page_suggestions)
119
+ failed_locator_value = failed_element_info[:selector_value]
120
+ failed_locator_type = failed_element_info[:selector_type]
121
+ return [] unless failed_locator_value && failed_locator_type
122
+
123
+ # Padroniza os tipos de localizador (ex: 'resource-id' vira 'id')
124
+ normalized_failed_type = failed_locator_type.downcase.include?('id') ? 'id' : failed_locator_type
125
+
126
+ cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
127
+ similarities = []
128
+
129
+ all_page_suggestions.each do |suggestion|
130
+ # Procura por um localizador na sugestão que tenha a MESMA ESTRATÉGIA do localizador que falhou
131
+ candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
132
+ next unless candidate_locator
163
133
 
164
- # Lógica aprimorada para comparação insensível a maiúsculas/minúsculas
165
- selector_value = failed_info[:selector_value].to_s.downcase
166
- is_similar = case platform
167
- when 'android'
168
- (attrs['resource-id']&.downcase&.include?(selector_value) ||
169
- attrs['text']&.downcase&.include?(selector_value) ||
170
- attrs['content-desc']&.downcase&.include?(selector_value))
171
- when 'ios'
172
- (attrs['accessibility-id']&.downcase&.include?(selector_value) ||
173
- attrs['label']&.downcase&.include?(selector_value) ||
174
- attrs['name']&.downcase&.include?(selector_value))
175
- else
176
- false
177
- end
178
-
179
- if is_similar
180
- name = self.suggest_name(node.name, attrs)
181
- locators = self.xpath_generator(node.name, attrs, platform)
182
- similar_elements << { name: name, locators: locators }
134
+ cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
135
+ distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
136
+ max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
137
+ next if max_len.zero?
138
+
139
+ similarity_score = 1.0 - (distance.to_f / max_len)
140
+ if similarity_score > 0.8 # Aumenta o limiar para maior precisão
141
+ similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score }
183
142
  end
184
143
  end
185
- similar_elements
144
+ similarities.sort_by { |s| -s[:score] }.first(5)
186
145
  end
187
146
 
147
+ # ... (get_all_elements_from_screen, truncate, suggest_name permanecem iguais) ...
148
+ def self.get_all_elements_from_screen(doc, platform)
149
+ seen_elements = {}
150
+ all_elements_suggestions = []
151
+ doc.xpath('//*').each do |node|
152
+ next if ['hierarchy', 'AppiumAUT'].include?(node.name)
153
+ attrs = node.attributes.transform_values(&:value)
154
+ unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
155
+ unless seen_elements[unique_key]
156
+ name = self.suggest_name(node.name, attrs)
157
+ locators = self.xpath_generator(node.name, attrs, platform)
158
+ all_elements_suggestions << { name: name, locators: locators }
159
+ seen_elements[unique_key] = true
160
+ end
161
+ end
162
+ all_elements_suggestions
163
+ end
164
+
188
165
  def self.truncate(value)
189
166
  return value unless value.is_a?(String)
190
167
  value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
@@ -195,7 +172,13 @@ module AppiumFailureHelper
195
172
  pfx = PREFIX[tag] || PREFIX[type] || 'elm'
196
173
  name_base = nil
197
174
 
198
- ['content-desc', 'text', 'resource-id', 'label', 'name'].each do |attr_key|
175
+ priority_attrs = if tag.start_with?('XCUIElementType')
176
+ ['name', 'label', 'value']
177
+ else
178
+ ['content-desc', 'text', 'resource-id']
179
+ end
180
+
181
+ priority_attrs.each do |attr_key|
199
182
  value = attrs[attr_key]
200
183
  if value.is_a?(String) && !value.empty?
201
184
  name_base = value
@@ -203,7 +186,8 @@ module AppiumFailureHelper
203
186
  end
204
187
  end
205
188
 
206
- name_base ||= type
189
+ # Se nenhum atributo de prioridade for encontrado, usa o nome da classe.
190
+ name_base ||= type.gsub('XCUIElementType', '') # Remove o prefixo longo do iOS
207
191
 
208
192
  truncated_name = truncate(name_base)
209
193
  sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
@@ -213,209 +197,150 @@ module AppiumFailureHelper
213
197
 
214
198
  def self.xpath_generator(tag, attrs, platform)
215
199
  case platform
216
- when 'android'
217
- self.generate_android_xpaths(tag, attrs)
218
- when 'ios'
219
- self.generate_ios_xpaths(tag, attrs)
220
- else
221
- self.generate_unknown_xpaths(tag, attrs)
200
+ when 'android' then self.generate_android_xpaths(tag, attrs)
201
+ when 'ios' then self.generate_ios_xpaths(tag, attrs)
202
+ else self.generate_unknown_xpaths(tag, attrs)
222
203
  end
223
204
  end
224
205
 
225
206
  def self.generate_android_xpaths(tag, attrs)
226
207
  locators = []
227
-
228
- if attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['text'] && !attrs['text'].empty?
229
- locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @text=\"#{self.truncate(attrs['text'])}\"]" }
230
- elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
231
- locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
232
- end
233
-
234
208
  if attrs['resource-id'] && !attrs['resource-id'].empty?
235
- locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\"]" }
209
+ # CORREÇÃO: A estratégia para resource-id é 'id', e o valor é o próprio ID.
210
+ locators << { strategy: 'id', locator: attrs['resource-id'] }
236
211
  end
237
-
238
- if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
239
- id_part = attrs['resource-id'].split(':id/').last
240
- locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, \"#{id_part}\")]" }
241
- end
242
-
243
212
  if attrs['text'] && !attrs['text'].empty?
244
- locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
213
+ locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{truncate(attrs['text'])}\"]" }
245
214
  end
246
215
  if attrs['content-desc'] && !attrs['content-desc'].empty?
247
- locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
216
+ locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{truncate(attrs['content-desc'])}\"]" }
248
217
  end
249
-
250
- locators << { strategy: 'generic_tag', locator: "//#{tag}" }
251
-
252
218
  locators
253
219
  end
254
220
 
255
221
  def self.generate_ios_xpaths(tag, attrs)
256
222
  locators = []
257
-
258
- if attrs['accessibility-id'] && !attrs['accessibility-id'].empty? && attrs['label'] && !attrs['label'].empty?
259
- locators << { strategy: 'accessibility_id_and_label', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\" and @label=\"#{self.truncate(attrs['label'])}\"]" }
260
- end
261
-
262
- if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
263
- locators << { strategy: 'accessibility_id', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\"]" }
223
+ if attrs['name'] && !attrs['name'].empty?
224
+ # CORREÇÃO: A estratégia para 'name' no iOS é 'name', e o valor é o próprio nome.
225
+ locators << { strategy: 'name', locator: attrs['name'] }
264
226
  end
265
-
266
227
  if attrs['label'] && !attrs['label'].empty?
267
- locators << { strategy: 'label', locator: "//#{tag}[@label=\"#{self.truncate(attrs['label'])}\"]" }
268
- end
269
- if attrs['name'] && !attrs['name'].empty?
270
- locators << { strategy: 'name', locator: "//#{tag}[@name=\"#{self.truncate(attrs['name'])}\"]" }
228
+ locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{truncate(attrs['label'])}\"]" }
271
229
  end
272
-
273
- locators << { strategy: 'generic_tag', locator: "//#{tag}" }
274
-
275
230
  locators
276
231
  end
277
-
232
+
278
233
  def self.generate_unknown_xpaths(tag, attrs)
279
- locators = []
280
- if attrs['resource-id'] && !attrs['resource-id'].empty?
281
- locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\"]" }
282
- end
283
- if attrs['content-desc'] && !attrs['content-desc'].empty?
284
- locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
285
- end
286
- if attrs['text'] && !attrs['text'].empty?
287
- locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
288
- end
289
-
290
- locators << { strategy: 'generic_tag', locator: "//#{tag}" }
291
-
292
- locators
234
+ locators = []
235
+ attrs.each { |key, value| locators << { strategy: key.to_s, locator: "//#{tag}[@#{key}=\"#{truncate(value)}\"]" } if value.is_a?(String) && !value.empty? }
236
+ locators
293
237
  end
294
238
 
295
239
  def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
296
-
240
+ # Lambdas para gerar partes do HTML
297
241
  locators_html = lambda do |locators|
298
- locators.map do |loc|
299
- "<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'>#{loc[:strategy].upcase.gsub('_', ' ')}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{loc[:locator]}</span></li>"
300
- end.join
242
+ 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].upcase.gsub('_', ' '))}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>" }.join
301
243
  end
302
244
 
303
245
  all_elements_html = lambda do |elements|
304
- elements.map do |el|
305
- "<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{el[:name]}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>"
306
- end.join
246
+ 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
307
247
  end
308
248
 
249
+ # Prepara o conteúdo dinâmico
309
250
  failed_info = targeted_report[:failed_element]
310
251
  similar_elements = targeted_report[:similar_elements]
311
-
312
- similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada. O elemento pode ter sido removido.</p>" : similar_elements.map { |el| "<div class='border border-indigo-100 p-3 rounded-lg bg-indigo-50'><p class='font-bold text-indigo-800 mb-2'>#{el[:name]}</p><ul>#{locators_html.call(el[:locators])}</ul></div>" }.join
313
-
252
+ de_para_analysis = targeted_report[:de_para_analysis]
253
+
254
+ # Bloco de HTML para a análise "De/Para"
255
+ de_para_html = ""
256
+ if de_para_analysis
257
+ de_para_html = <<~HTML
258
+ <div class="bg-green-50 border border-green-200 p-4 rounded-lg shadow-md mb-6">
259
+ <h3 class="text-lg font-bold text-green-800 mb-2">Análise de Mapeamento (De/Para)</h3>
260
+ <p class="text-sm text-gray-700 mb-1">O nome lógico <strong class="font-mono bg-gray-200 px-1 rounded">#{CGI.escapeHTML(de_para_analysis[:logical_name])}</strong> foi encontrado nos seus arquivos locais!</p>
261
+ <p class="text-sm text-gray-700">O localizador correto definido é:</p>
262
+ <div class="font-mono text-xs bg-green-100 p-2 mt-2 rounded">
263
+ <span class="font-bold">#{CGI.escapeHTML(de_para_analysis[:correct_locator]['tipoBusca'].upcase)}:</span>
264
+ <span class="break-all">#{CGI.escapeHTML(de_para_analysis[:correct_locator]['valor'])}</span>
265
+ </div>
266
+ </div>
267
+ HTML
268
+ end
269
+
270
+ similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>" : similar_elements.map { |el|
271
+ score_percent = (el[:score] * 100).round(1)
272
+ "<div class='border border-indigo-100 p-3 rounded-lg bg-indigo-50'><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'>Similaridade: #{score_percent}%</span></p><ul>#{locators_html.call(el[:locators])}</ul></div>"
273
+ }.join
274
+
314
275
  failed_info_content = if failed_info && failed_info[:selector_value]
315
- "<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'>#{failed_info[:selector_type]}</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'>#{failed_info[:selector_value]}</span></p>"
276
+ "<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>"
316
277
  else
317
278
  "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
318
279
  end
319
280
 
320
- # Template HTML usando um heredoc
281
+ # Template HTML completo
321
282
  <<~HTML_REPORT
322
283
  <!DOCTYPE html>
323
284
  <html lang="pt-BR">
324
285
  <head>
325
- <meta charset="UTF-8">
326
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
327
- <title>Relatório de Falha Appium - #{timestamp}</title>
328
- <script src="https://cdn.tailwindcss.com"></script>
329
- <style>
330
- body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
331
- .tab-content { display: none; }
332
- .tab-content.active { display: block; }
333
- .tab-button.active { background-color: #4f46e5; color: white; }
334
- .tab-button:not(.active):hover { background-color: #e0e7ff; }
335
- </style>
286
+ <meta charset="UTF-8">
287
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
288
+ <title>Relatório de Falha Appium - #{timestamp}</title>
289
+ <script src="https://cdn.tailwindcss.com"></script>
290
+ <style> body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; } .tab-content { display: none; } .tab-content.active { display: block; } .tab-button.active { background-color: #4f46e5; color: white; } </style>
336
291
  </head>
337
292
  <body class="bg-gray-50 p-8">
338
- <div class="max-w-7xl mx-auto">
339
- <header class="mb-8 pb-4 border-b border-gray-300">
340
- <h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
341
- <p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
342
- </header>
343
-
344
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
345
- <!-- Coluna de Screenshots e Falha -->
346
- <div class="lg:col-span-1">
347
- <div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
348
- <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
349
- #{failed_info_content}
350
- </div>
351
-
352
- <div class="bg-white p-4 rounded-lg shadow-xl">
353
- <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
354
- <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
355
- </div>
293
+ <div class="max-w-7xl mx-auto">
294
+ <header class="mb-8 pb-4 border-b border-gray-300">
295
+ <h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
296
+ <p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
297
+ </header>
298
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
299
+ <div class="lg:col-span-1">
300
+ #{de_para_html}
301
+ <div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
302
+ <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
303
+ #{failed_info_content}
304
+ </div>
305
+ <div class="bg-white p-4 rounded-lg shadow-xl">
306
+ <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
307
+ <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
308
+ </div>
309
+ </div>
310
+ <div class="lg:col-span-2">
311
+ <div class="bg-white rounded-lg shadow-xl">
312
+ <div class="flex border-b border-gray-200">
313
+ <button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{similar_elements.size})</button>
314
+ <button class="tab-button px-4 py-3 text-sm font-medium text-gray-600" data-tab="all">Dump Completo da Página (#{all_suggestions.size} Elementos)</button>
315
+ </div>
316
+ <div class="p-6">
317
+ <div id="similar" class="tab-content active">
318
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
319
+ <div class="space-y-4">#{similar_elements_content}</div>
356
320
  </div>
357
-
358
- <!-- Coluna de Relatórios e Sugestões -->
359
- <div class="lg:col-span-2">
360
- <div class="bg-white rounded-lg shadow-xl">
361
- <!-- Abas de Navegação -->
362
- <div class="flex border-b border-gray-200">
363
- <button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{similar_elements.size})</button>
364
- <button class="tab-button px-4 py-3 text-sm font-medium text-gray-600" data-tab="all">Dump Completo da Página (#{all_suggestions.size} Elementos)</button>
365
- </div>
366
-
367
- <!-- Conteúdo das Abas -->
368
- <div class="p-6">
369
- <!-- Aba Sugestões de Reparo -->
370
- <div id="similar" class="tab-content active">
371
- <h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
372
- <div class="space-y-4">
373
- #{similar_elements_content}
374
- </div>
375
- </div>
376
-
377
- <!-- Aba Dump Completo -->
378
- <div id="all" class="tab-content">
379
- <h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
380
- <div class="max-h-[600px] overflow-y-auto space-y-2">
381
- #{all_elements_html.call(all_suggestions)}
382
- </div>
383
- </div>
384
- </div>
385
- </div>
321
+ <div id="all" class="tab-content">
322
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump de Todos os Elementos da Tela</h3>
323
+ <div class="max-h-[600px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
386
324
  </div>
325
+ </div>
387
326
  </div>
327
+ </div>
388
328
  </div>
389
-
390
- <script>
391
- document.addEventListener('DOMContentLoaded', () => {
392
- const tabs = document.querySelectorAll('.tab-button');
393
- const contents = document.querySelectorAll('.tab-content');
394
-
395
- tabs.forEach(tab => {
396
- tab.addEventListener('click', () => {
397
- const target = tab.getAttribute('data-tab');
398
-
399
- tabs.forEach(t => {
400
- t.classList.remove('active', 'text-white', 'bg-indigo-600');
401
- t.classList.add('text-gray-600');
402
- });
403
- contents.forEach(c => c.classList.remove('active'));
404
-
405
- tab.classList.add('active', 'text-white', 'bg-indigo-600');
406
- tab.classList.remove('text-gray-600');
407
- document.getElementById(target).classList.add('active');
408
- });
409
- });
410
-
411
- // Set initial active state for styling consistency
412
- const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
413
- activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
414
- });
415
- </script>
329
+ </div>
330
+ <script>
331
+ document.querySelectorAll('.tab-button').forEach(tab => {
332
+ tab.addEventListener('click', () => {
333
+ const target = tab.getAttribute('data-tab');
334
+ document.querySelectorAll('.tab-button').forEach(t => t.classList.remove('active'));
335
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
336
+ tab.classList.add('active');
337
+ document.getElementById(target).classList.add('active');
338
+ });
339
+ });
340
+ </script>
416
341
  </body>
417
342
  </html>
418
343
  HTML_REPORT
419
344
  end
420
345
  end
421
- end
346
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppiumFailureHelper
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.2"
5
5
  end
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: 0.6.0
4
+ version: 0.6.2
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-09-23 00:00:00.000000000 Z
11
+ date: 2025-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri