appium_failure_helper 1.0.0 → 1.1.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: fcada9d17369c2f0afdc27bab51a796c6abd060557783155fd49863bec12f10a
4
- data.tar.gz: 0f3cd2fbc47011c5c943ac8e9e573f62c5c15cd7727ef1b9ff1837f9db8050c3
3
+ metadata.gz: 206d70f79fcfb3cf01291da330e02da83218080d511f02ec899c9778e12ccec0
4
+ data.tar.gz: 2dbc0c9b407e44f786353b6914faf8c9baa0c2014fe3f84fd1e3532f45837dad
5
5
  SHA512:
6
- metadata.gz: f93b8cb2b8acf629cad21a7de037d793f0a6a9415a674a110e407c13a56914570b5ddb59e457adaf79869b0903ce5d102cc56c97094207e5c476086b20241b71
7
- data.tar.gz: b7f6b505d3864fb3079bd6a7985ed0dd02756861d743abde308c6e78928c86de10a80341f17379fdab10a5bb57d31db0375860636b83470cb91147f0ed8791ab
6
+ metadata.gz: c2a12c07420ec30127d4297258407f5e90f33dbcb395143a8b6332ec97b29ac704e2022ec86e661d1416706bbc6549b6dfdc77f70265ed7dc065f7e74ea9eff3
7
+ data.tar.gz: c52d7bb4ac02b470063866bb959a9067342d24f4699f9aba856b8a6d8a30fd388ed6aca9434dd397136cb07faec4adc567751856182ede3196cbbd0348b3126f
@@ -1,6 +1,35 @@
1
- # lib/appium_failure_helper/analyzer.rb
2
1
  module AppiumFailureHelper
3
2
  module Analyzer
3
+ def self.triage_error(exception)
4
+ rspec_error_class = defined?(RSpec::Expectations::ExpectationNotMetError) ? RSpec::Expectations::ExpectationNotMetError : Class.new
5
+
6
+ result = case exception
7
+ when Selenium::WebDriver::Error::NoSuchElementError,
8
+ Selenium::WebDriver::Error::TimeoutError,
9
+ Selenium::WebDriver::Error::UnknownCommandError
10
+ :locator_issue
11
+ when Selenium::WebDriver::Error::ElementNotInteractableError
12
+ :visibility_issue
13
+ when Selenium::WebDriver::Error::StaleElementReferenceError
14
+ :stale_element_issue
15
+ when rspec_error_class
16
+ :assertion_failure
17
+ when NoMethodError, NameError, ArgumentError, TypeError
18
+ :ruby_code_issue
19
+ when Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
20
+ :session_startup_issue
21
+ when Selenium::WebDriver::Error::WebDriverError
22
+ if exception.message.include?('session deleted because of page crash')
23
+ :app_crash_issue
24
+ else
25
+ :unknown_appium_issue
26
+ end
27
+ else
28
+ :unknown_issue
29
+ end
30
+ return result
31
+ end
32
+
4
33
  def self.extract_failure_details(exception)
5
34
  message = exception.message
6
35
  info = {}
@@ -11,36 +40,31 @@ module AppiumFailureHelper
11
40
  /(?:with the resource-id|with the accessibility-id) ['"]?(.+?)['"]?/i
12
41
  ]
13
42
  patterns.each do |pattern|
14
- match = message.match(pattern)
15
- if match
16
- info[:selector_value] = match.captures.last.strip.gsub(/['"]/, '')
17
- info[:selector_type] = match.captures.size > 1 ? match.captures[0].strip.gsub(/['"]/, '') : 'id'
18
- return info
19
- end
43
+ match = message.match(pattern)
44
+ if match
45
+ info[:selector_value] = match.captures.last.strip.gsub(/['"]/, '')
46
+ info[:selector_type] = match.captures.size > 1 ? match.captures[0].strip.gsub(/['"]/, '') : 'id'
47
+ return info
48
+ end
20
49
  end
21
50
  info
22
51
  end
23
-
52
+
24
53
  def self.find_de_para_match(failed_info, element_map)
25
54
  failed_value = failed_info[:selector_value].to_s
26
55
  return nil if failed_value.empty?
27
-
28
56
  logical_name_key = failed_value.gsub(/^#/, '')
29
-
30
57
  if element_map.key?(logical_name_key)
31
58
  return { logical_name: logical_name_key, correct_locator: element_map[logical_name_key] }
32
59
  end
33
-
34
60
  cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
35
-
36
61
  element_map.each do |name, locator_info|
37
- mapped_locator = locator_info['valor'].to_s
62
+ mapped_locator = locator_info['valor'].to_s || locator_info['value'].to_s
38
63
  cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
39
64
  distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
40
65
  max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
41
66
  next if max_len.zero?
42
67
  similarity_score = 1.0 - (distance.to_f / max_len)
43
-
44
68
  if similarity_score > 0.85
45
69
  return { logical_name: name, correct_locator: locator_info }
46
70
  end
@@ -52,20 +76,16 @@ module AppiumFailureHelper
52
76
  failed_locator_value = failed_info[:selector_value]
53
77
  failed_locator_type = failed_info[:selector_type]
54
78
  return [] unless failed_locator_value && failed_locator_type
55
-
56
79
  normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
57
80
  cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
58
81
  similarities = []
59
-
60
82
  all_page_suggestions.each do |suggestion|
61
83
  candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
62
84
  next unless candidate_locator
63
-
64
85
  cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
65
86
  distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
66
87
  max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
67
88
  next if max_len.zero?
68
-
69
89
  similarity_score = 1.0 - (distance.to_f / max_len)
70
90
  if similarity_score > 0.85
71
91
  similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score, attributes: suggestion[:attributes] }
@@ -24,7 +24,13 @@ module AppiumFailureHelper
24
24
  return map
25
25
  end
26
26
  instance.elements.each do |key, value|
27
- map[key.to_s] = { 'tipoBusca' => value[0], 'valor' => value[1] }
27
+ valor = value[1]
28
+ if valor.is_a?(Hash)
29
+ valor_final = valor['valor'] || valor['value'] || valor
30
+ else
31
+ valor_final = valor
32
+ end
33
+ map[key.to_s] = { 'tipoBusca' => value[0], 'valor' => valor_final }
28
34
  end
29
35
  rescue => e
30
36
  Utils.logger.warn("AVISO: Erro ao processar o arquivo #{file_path}: #{e.message}")
@@ -1,3 +1,4 @@
1
+ # lib/appium_failure_helper/handler.rb
1
2
  module AppiumFailureHelper
2
3
  class Handler
3
4
  def self.call(driver, exception)
@@ -12,66 +13,72 @@ module AppiumFailureHelper
12
13
  end
13
14
 
14
15
  def call
15
- unless @driver && @driver.session_id
16
- Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
17
- Utils.logger.error("Exceção original: #{@exception.message}")
18
- return
19
- end
20
-
21
- FileUtils.mkdir_p(@output_folder)
22
- page_source = @driver.page_source
23
- platform_value = @driver.capabilities['platformName'] || @driver.capabilities[:platformName]
24
- platform = platform_value&.downcase || 'unknown'
25
-
26
- @doc = Nokogiri::XML(page_source)
16
+ begin
17
+ unless @driver && @driver.session_id
18
+ Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
19
+ Utils.logger.error("Exceção original: #{@exception.message}")
20
+ return
21
+ end
27
22
 
28
- failed_info = Analyzer.extract_failure_details(@exception) || {}
29
- if failed_info.empty?
30
- Utils.logger.info("Análise da mensagem de erro falhou. Tentando analisar código-fonte...")
31
- failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
32
- end
33
-
34
- page_analyzer = PageAnalyzer.new(page_source, platform)
35
- all_page_elements = page_analyzer.analyze || []
23
+ FileUtils.mkdir_p(@output_folder)
24
+
25
+ triage_result = Analyzer.triage_error(@exception)
26
+
27
+ report_data = {
28
+ exception: @exception,
29
+ triage_result: triage_result,
30
+ timestamp: @timestamp,
31
+ platform: @driver.capabilities['platformName'] || @driver.capabilities[:platformName] || 'unknown',
32
+ screenshot_base64: @driver.screenshot_as(:base64)
33
+ }
36
34
 
37
- similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
35
+ if triage_result == :locator_issue
36
+ page_source = @driver.page_source
37
+ doc = Nokogiri::XML(page_source)
38
38
 
39
- alternative_xpaths = []
40
- if !similar_elements.empty?
41
- target_suggestion = similar_elements.first
42
-
43
- if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
44
- target_node = @doc.at_xpath(target_path)
39
+ failed_info = Analyzer.extract_failure_details(@exception) || {}
40
+ if failed_info.empty?
41
+ failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
42
+ end
45
43
 
46
- alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
47
- end
48
- end
44
+ if failed_info.empty?
45
+ report_data[:triage_result] = :unidentified_locator_issue
46
+ else
47
+ page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
48
+ all_page_elements = page_analyzer.analyze || []
49
+ similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
50
+
51
+ alternative_xpaths = []
52
+ if !similar_elements.empty?
53
+ target_suggestion = similar_elements.first
54
+ if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
55
+ target_node = doc.at_xpath(target_path)
56
+ alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
57
+ end
58
+ end
49
59
 
50
- unified_element_map = ElementRepository.load_all
51
- de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
52
- code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
60
+ unified_element_map = ElementRepository.load_all
61
+ de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
62
+ code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
53
63
 
54
- report_data = {
55
- failed_element: failed_info,
56
- similar_elements: similar_elements,
57
- alternative_xpaths: alternative_xpaths,
58
- de_para_analysis: de_para_result,
59
- code_search_results: code_search_results,
60
- all_page_elements: all_page_elements,
61
- screenshot_base64: @driver.screenshot_as(:base64),
62
- platform: platform,
63
- timestamp: @timestamp
64
- }
64
+ report_data.merge!({
65
+ page_source: page_source,
66
+ failed_element: failed_info,
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
+ })
73
+ end
74
+ end
65
75
 
66
- ReportGenerator.new(@output_folder, page_source, report_data).generate_all
67
- Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
68
-
69
- rescue => e
70
- puts "--- ERRO FATAL NA GEM (DIAGNÓSTICO) ---"
71
- puts "CLASSE DO ERRO: #{e.class}"
72
- puts "MENSAGEM: #{e.message}"
73
- puts "BACKTRACE:\n#{e.backtrace.join("\n")}"
74
- puts "----------------------------------------"
76
+ ReportGenerator.new(@output_folder, report_data).generate_all
77
+ Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
78
+
79
+ rescue => e
80
+ Utils.logger.error("Erro fatal na GEM de diagnóstico: #{e.message}\n#{e.backtrace.join("\n")}")
81
+ end
75
82
  end
76
83
  end
77
84
  end
@@ -1,13 +1,13 @@
1
1
  module AppiumFailureHelper
2
2
  class ReportGenerator
3
- def initialize(output_folder, page_source, report_data)
3
+ def initialize(output_folder, report_data)
4
4
  @output_folder = output_folder
5
- @page_source = page_source
6
5
  @data = report_data
6
+ @page_source = report_data[:page_source] # Pega o page_source de dentro do hash
7
7
  end
8
8
 
9
9
  def generate_all
10
- generate_xml_report
10
+ generate_xml_report if @page_source
11
11
  generate_yaml_reports
12
12
  generate_html_report
13
13
  end
@@ -19,18 +19,75 @@ module AppiumFailureHelper
19
19
  end
20
20
 
21
21
  def generate_yaml_reports
22
+ # Gera um YAML simplificado se não for um problema de seletor
22
23
  analysis_report = {
24
+ triage_result: @data[:triage_result],
25
+ exception_class: @data[:exception].class.to_s,
26
+ exception_message: @data[:exception].message,
23
27
  failed_element: @data[:failed_element],
24
28
  similar_elements: @data[:similar_elements],
25
29
  de_para_analysis: @data[:de_para_analysis],
26
- code_search_results: @data[:code_search_results],
27
- alternative_xpaths: @data[:alternative_xpaths]
30
+ code_search_results: @data[:code_search_results]
28
31
  }
29
32
  File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
30
- File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
33
+
34
+ # Só gera o dump de elementos se a análise completa tiver sido feita
35
+ if @data[:all_page_elements]
36
+ File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
37
+ end
31
38
  end
32
39
 
33
40
  def generate_html_report
41
+ if @data[:triage_result] == :locator_issue && @data[:failed_element].empty?
42
+ return build_simple_diagnosis_report(
43
+ title: "Falha na Análise do Seletor",
44
+ message: "A GEM identificou um erro de 'elemento não encontrado', mas não conseguiu extrair o seletor da mensagem de erro ou do código-fonte. Isso pode ocorrer com métodos de busca customizados ou seletores dinâmicos. Verifique o stack trace para encontrar a linha exata do erro e o método responsável."
45
+ )
46
+ end
47
+ html_content = case @data[:triage_result]
48
+ when :locator_issue
49
+ build_full_report
50
+ when :unidentified_locator_issue, :unidentified_timeout_issue
51
+ build_simple_diagnosis_report(
52
+ title: "Seletor Não Identificado",
53
+ message: "A falha ocorreu porque um elemento não foi encontrado, mas a GEM não conseguiu extrair o seletor exato da mensagem de erro ou do código-fonte. Isso geralmente acontece quando o seletor é construído dinamicamente ou está dentro de um método helper complexo. Verifique o stack trace para encontrar o método responsável (ex: 'tap_by_text')."
54
+ )
55
+ when :assertion_failure
56
+ build_simple_diagnosis_report(
57
+ title: "Falha de Asserção (Bug Funcional)",
58
+ message: "A automação executou os passos corretamente, mas o resultado final verificado na tela não foi o esperado. Isso geralmente indica um bug funcional na aplicação, e não um problema com o seletor."
59
+ )
60
+ when :visibility_issue
61
+ build_simple_diagnosis_report(
62
+ title: "Elemento Oculto ou Não-Interagível",
63
+ message: "O seletor encontrou o elemento no XML da página, mas ele não está visível ou habilitado para interação. Verifique se há outros elementos sobrepondo-o, se ele está desabilitado (disabled/enabled='false'), ou se é necessário aguardar uma animação."
64
+ )
65
+ when :stale_element_issue
66
+ build_simple_diagnosis_report(
67
+ title: "Referência de Elemento Antiga (Stale)",
68
+ message: "O elemento foi encontrado, mas a página foi atualizada antes que a interação pudesse ocorrer. Isso é um problema de timing. A solução é encontrar o elemento novamente logo antes de interagir com ele."
69
+ )
70
+ when :session_startup_issue
71
+ build_simple_diagnosis_report(
72
+ title: "Falha na Conexão com o Servidor Appium",
73
+ message: "Não foi possível criar uma sessão com o servidor. Verifique se o servidor Appium está rodando, se as 'capabilities' (incluindo prefixos 'appium:') e a URL de conexão estão corretas."
74
+ )
75
+ when :app_crash_issue
76
+ build_simple_diagnosis_report(
77
+ title: "Crash do Aplicativo",
78
+ message: "A sessão foi encerrada inesperadamente, o que indica que o aplicativo travou. A causa raiz deve ser investigada nos logs do dispositivo (Logcat para Android, Console para iOS)."
79
+ )
80
+ else # :ruby_code_issue, :unknown_issue
81
+ build_simple_diagnosis_report(
82
+ title: "Erro no Código de Teste",
83
+ message: "A falha foi causada por um erro de sintaxe ou lógica no próprio código de automação (ex: método não definido, variável nula). O problema não é no Appium ou no seletor, mas sim no script. Verifique o stack trace para encontrar o arquivo e a linha exatos."
84
+ )
85
+ end
86
+
87
+ File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
88
+ end
89
+
90
+ def build_full_report
34
91
  failed_info = @data[:failed_element] || {}
35
92
  similar_elements = @data[:similar_elements] || []
36
93
  all_suggestions = @data[:all_page_elements] || []
@@ -40,21 +97,19 @@ module AppiumFailureHelper
40
97
  timestamp = @data[:timestamp]
41
98
  platform = @data[:platform]
42
99
  screenshot_base64 = @data[:screenshot_base64]
43
-
100
+
44
101
  locators_html = lambda do |locators|
45
- (locators || []).map do |loc|
46
- strategy_text = loc[:strategy].to_s.upcase.gsub('_', ' ')
47
- "<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>"
48
- end.join
102
+ (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
49
103
  end
50
104
 
51
105
  all_elements_html = lambda do |elements|
52
106
  (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
53
107
  end
54
108
 
55
- de_para_html = ""
56
- failed_info_content = ""
57
-
109
+ de_para_html = "" # (Sua lógica de_para_html)
110
+ code_search_html = "" # (Sua lógica code_search_html)
111
+ failed_info_content = if failed_info && !failed_info.empty?; # ... (Sua lógica failed_info_content)
112
+ else "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"; end
58
113
  code_search_html = ""
59
114
  unless code_search_results.empty?
60
115
  suggestions_list = code_search_results.map do |match|
@@ -75,13 +130,14 @@ module AppiumFailureHelper
75
130
  HTML
76
131
  end
77
132
 
133
+ # --- LÓGICA RESTAURADA: ELEMENTO COM FALHA ---
78
134
  failed_info_content = if failed_info && !failed_info.empty?
79
135
  "<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>"
80
136
  else
81
137
  "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
82
138
  end
83
139
 
84
- repair_strategies_content = if alternative_xpaths.empty?
140
+ repair_strategies_content = if alternative_xpaths.empty?
85
141
  "<p class='text-gray-500'>Nenhuma estratégia de XPath alternativa pôde ser gerada para o elemento alvo.</p>"
86
142
  else
87
143
  pages = alternative_xpaths.each_slice(6).to_a
@@ -152,7 +208,7 @@ module AppiumFailureHelper
152
208
  CAROUSEL
153
209
  end
154
210
 
155
- html_content = <<~HTML_REPORT
211
+ <<~HTML_REPORT
156
212
  <!DOCTYPE html>
157
213
  <html lang="pt-BR">
158
214
  <head>
@@ -199,7 +255,7 @@ module AppiumFailureHelper
199
255
  </div>
200
256
  </div>
201
257
  </div>
202
- <script>
258
+ <script>
203
259
  document.addEventListener('DOMContentLoaded', () => {
204
260
  const tabs = document.querySelectorAll('.tab-button');
205
261
  tabs.forEach(tab => {
@@ -253,8 +309,56 @@ module AppiumFailureHelper
253
309
  </body>
254
310
  </html>
255
311
  HTML_REPORT
312
+ end
256
313
 
257
- File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
314
+ def build_simple_diagnosis_report(title:, message:)
315
+ exception = @data[:exception]
316
+ error_message_html = CGI.escapeHTML(exception.message.to_s)
317
+ backtrace_html = CGI.escapeHTML(exception.backtrace.join("\n"))
318
+
319
+ <<~HTML_REPORT
320
+ <!DOCTYPE html>
321
+ <html lang="pt-BR">
322
+ <head>
323
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
324
+ <title>Diagnóstico de Falha - #{title}</title>
325
+ <script src="https://cdn.tailwindcss.com"></script>
326
+ </head>
327
+ <body class="bg-gray-100 p-4 sm:p-8">
328
+ <div class="max-w-4xl mx-auto">
329
+ <header class="mb-8 pb-4 border-b border-gray-200">
330
+ <h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
331
+ <p class="text-sm text-gray-500">Relatório gerado em: #{@data[:timestamp]} | Plataforma: #{@data[:platform].to_s.upcase}</p>
332
+ </header>
333
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
334
+ <div class="md:col-span-1">
335
+ <div class="bg-white p-4 rounded-lg shadow-md">
336
+ <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
337
+ <img src="data:image/png;base64,#{@data[:screenshot_base64]}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
338
+ </div>
339
+ </div>
340
+ <div class="md:col-span-2 space-y-6">
341
+ <div class="bg-white p-6 rounded-lg shadow-md">
342
+ <h2 class="text-xl font-bold text-red-600 mb-4">Diagnóstico: #{title}</h2>
343
+ <div class="bg-red-50 border-l-4 border-red-500 text-red-800 p-4 rounded-r-lg">
344
+ <p class="font-semibold">Causa Provável:</p>
345
+ <p>#{message}</p>
346
+ </div>
347
+ </div>
348
+ <div class="bg-white p-6 rounded-lg shadow-md">
349
+ <h3 class="text-lg font-semibold text-gray-700 mb-2">Mensagem de Erro Original</h3>
350
+ <pre class="bg-gray-800 text-white p-4 rounded text-xs whitespace-pre-wrap break-words max-h-48 overflow-y-auto"><code>#{error_message_html}</code></pre>
351
+ </div>
352
+ <div class="bg-white p-6 rounded-lg shadow-md">
353
+ <h3 class="text-lg font-semibold text-gray-700 mb-2">Stack Trace</h3>
354
+ <pre class="bg-gray-800 text-white p-4 rounded text-xs whitespace-pre-wrap break-words max-h-72 overflow-y-auto"><code>#{backtrace_html}</code></pre>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </body>
360
+ </html>
361
+ HTML_REPORT
258
362
  end
259
363
  end
260
364
  end
@@ -1,26 +1,26 @@
1
+ # lib/appium_failure_helper/source_code_analyzer.rb
1
2
  module AppiumFailureHelper
2
3
  module SourceCodeAnalyzer
4
+ # VERSÃO 3.0: Padrões de Regex mais flexíveis que aceitam um "receptor" opcional (como $driver).
3
5
  PATTERNS = [
4
- { type: 'id', regex: /find_element\((?:id:|:id\s*=>)\s*['"]([^'"]+)['"]\)/ },
5
- { type: 'xpath', regex: /find_element\((?:xpath:|:xpath\s*=>)\s*['"]([^'"]+)['"]\)/ },
6
- { type: 'accessibility_id', regex: /find_element\((?:accessibility_id:|:accessibility_id\s*=>)\s*['"]([^'"]+)['"]\)/ },
7
- { type: 'class_name', regex: /find_element\((?:class_name:|:class_name\s*=>)\s*['"]([^'"]+)['"]\)/ },
8
- { type: 'xpath', regex: /find_element\(:xpath,\s*['"]([^'"]+)['"]\)/ },
9
- { type: 'id', regex: /\s(?:id)\s*\(?['"]([^'"]+)['"]\)?/ },
10
- { type: 'xpath', regex: /\s(?:xpath)\s*\(?['"]([^'"]+)['"]\)?/ },
11
- { type: 'accessibility_id', regex: /\s(?:accessibility_id)\s*\(?['"]([^'"]+)['"]\)?/ }
6
+ { type: 'id', regex: /(?:\$driver\.)?find_element\((?:id:|:id\s*=>)\s*['"]([^'"]+)['"]\)/ },
7
+ { type: 'xpath', regex: /(?:\$driver\.)?find_element\((?:xpath:|:xpath\s*=>)\s*['"]([^'"]+)['"]\)/ },
8
+ { type: 'accessibility_id', regex: /(?:\$driver\.)?find_element\((?:accessibility_id:|:accessibility_id\s*=>)\s*['"]([^'"]+)['"]\)/ },
9
+ { type: 'class_name', regex: /(?:\$driver\.)?find_element\((?:class_name:|:class_name\s*=>)\s*['"]([^'"]+)['"]\)/ },
10
+ { type: 'xpath', regex: /(?:\$driver\.)?find_element\(:xpath,\s*['"]([^'"]+)['"]\)/ },
11
+ { type: 'id', regex: /(?:\$driver\.)?\s*id\s*\(?['"]([^'"]+)['"]\)?/ },
12
+ { type: 'xpath', regex: /(?:\$driver\.)?\s*xpath\s*\(?['"]([^'"]+)['"]\)?/ }
12
13
  ].freeze
13
14
 
14
15
  def self.extract_from_exception(exception)
16
+ # Busca a primeira linha do backtrace que seja um arquivo .rb do projeto
15
17
  location = exception.backtrace.find { |line| line.include?('.rb') && !line.include?('gems') }
16
18
  return {} unless location
17
19
 
18
20
  path_match = location.match(/^(.*?):(\d+)(?::in.*)?$/)
19
21
  return {} unless path_match
20
22
 
21
- file_path = path_match[1]
22
- line_number = path_match[2]
23
-
23
+ file_path, line_number = path_match.captures
24
24
  return {} unless File.exist?(file_path)
25
25
 
26
26
  begin
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appium_failure_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento