appium_failure_helper 0.6.8 → 1.0.0

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: a599f36b97e9ca76cfe763ad26526ea1b80e829128e95667b1020bf29cb9cb41
4
- data.tar.gz: 58f249a54a0410b9927bbb14b4551375cd77de49eb35db282cad16a9e0b7817d
3
+ metadata.gz: fcada9d17369c2f0afdc27bab51a796c6abd060557783155fd49863bec12f10a
4
+ data.tar.gz: 0f3cd2fbc47011c5c943ac8e9e573f62c5c15cd7727ef1b9ff1837f9db8050c3
5
5
  SHA512:
6
- metadata.gz: b9c4ab54799677d260fa7939705fc074a2e9233026b696d2d4548da344b0464f8dcbbda5016a735939819e04c2b06e700ed14d23cde1c8ac55cbfb844cae9b10
7
- data.tar.gz: d076e206ca68bd47560a95162428b1dc090df4890934f535de0c68f98b17206adb7229980175c1773dd542e65ac01dd9accbf4da83104cbb68ad893c0df773e0
6
+ metadata.gz: f93b8cb2b8acf629cad21a7de037d793f0a6a9415a674a110e407c13a56914570b5ddb59e457adaf79869b0903ce5d102cc56c97094207e5c476086b20241b71
7
+ data.tar.gz: b7f6b505d3864fb3079bd6a7985ed0dd02756861d743abde308c6e78928c86de10a80341f17379fdab10a5bb57d31db0375860636b83470cb91147f0ed8791ab
@@ -1,3 +1,4 @@
1
+ # lib/appium_failure_helper/analyzer.rb
1
2
  module AppiumFailureHelper
2
3
  module Analyzer
3
4
  def self.extract_failure_details(exception)
@@ -19,14 +20,30 @@ module AppiumFailureHelper
19
20
  end
20
21
  info
21
22
  end
22
-
23
+
23
24
  def self.find_de_para_match(failed_info, element_map)
24
- logical_name_key = failed_info[:selector_value].to_s.gsub(/^#/, '')
25
+ failed_value = failed_info[:selector_value].to_s
26
+ return nil if failed_value.empty?
27
+
28
+ logical_name_key = failed_value.gsub(/^#/, '')
29
+
25
30
  if element_map.key?(logical_name_key)
26
- return {
27
- logical_name: logical_name_key,
28
- correct_locator: element_map[logical_name_key]
29
- }
31
+ return { logical_name: logical_name_key, correct_locator: element_map[logical_name_key] }
32
+ end
33
+
34
+ cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
35
+
36
+ element_map.each do |name, locator_info|
37
+ mapped_locator = locator_info['valor'].to_s
38
+ cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
39
+ distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
40
+ max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
41
+ next if max_len.zero?
42
+ similarity_score = 1.0 - (distance.to_f / max_len)
43
+
44
+ if similarity_score > 0.85
45
+ return { logical_name: name, correct_locator: locator_info }
46
+ end
30
47
  end
31
48
  nil
32
49
  end
@@ -36,8 +53,7 @@ module AppiumFailureHelper
36
53
  failed_locator_type = failed_info[:selector_type]
37
54
  return [] unless failed_locator_value && failed_locator_type
38
55
 
39
- normalized_failed_type = failed_locator_type.downcase.include?('id') ? 'id' : failed_locator_type
40
-
56
+ normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
41
57
  cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
42
58
  similarities = []
43
59
 
@@ -51,8 +67,8 @@ module AppiumFailureHelper
51
67
  next if max_len.zero?
52
68
 
53
69
  similarity_score = 1.0 - (distance.to_f / max_len)
54
- if similarity_score > 0.8
55
- similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score }
70
+ if similarity_score > 0.85
71
+ similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score, attributes: suggestion[:attributes] }
56
72
  end
57
73
  end
58
74
  similarities.sort_by { |s| -s[:score] }.first(5)
@@ -0,0 +1,44 @@
1
+ module AppiumFailureHelper
2
+ module CodeSearcher
3
+ def self.find_similar_locators(failed_info)
4
+ failed_locator_value = failed_info[:selector_value]
5
+ return [] if failed_locator_value.nil? || failed_locator_value.empty?
6
+
7
+ cleaned_failed_locator = failed_locator_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
8
+ best_matches = []
9
+
10
+ Dir.glob(File.join('features', '**', '*.rb')).each do |file_path|
11
+ next if file_path.include?('gems')
12
+
13
+ begin
14
+ File.foreach(file_path).with_index do |line, line_num|
15
+ line.scan(/['"]([^'"]+)['"]/).flatten.each do |found_locator|
16
+ cleaned_found_locator = found_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
17
+ next if cleaned_found_locator.length < 5
18
+
19
+ distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_found_locator)
20
+ max_len = [cleaned_failed_locator.length, cleaned_found_locator.length].max
21
+ next if max_len.zero?
22
+
23
+ similarity_score = 1.0 - (distance.to_f / max_len)
24
+
25
+ if similarity_score > 0.85
26
+ best_matches << {
27
+ score: similarity_score,
28
+ file: file_path,
29
+ line_number: line_num + 1,
30
+ code: line.strip,
31
+ found_locator: found_locator
32
+ }
33
+ end
34
+ end
35
+ end
36
+ rescue ArgumentError
37
+ next
38
+ end
39
+ end
40
+
41
+ best_matches.sort_by { |m| -m[:score] }.first(3)
42
+ end
43
+ end
44
+ end
@@ -11,13 +11,11 @@ module AppiumFailureHelper
11
11
 
12
12
  def self.load_from_ruby_file
13
13
  map = {}
14
- # ALTERADO: Lê os caminhos a partir da configuração central.
15
14
  config = AppiumFailureHelper.configuration
16
15
  file_path = File.join(Dir.pwd, config.elements_path, config.elements_ruby_file)
17
16
 
18
17
  return map unless File.exist?(file_path)
19
18
 
20
- # ... (o resto do método continua igual)
21
19
  begin
22
20
  require file_path
23
21
  instance = OnboardingElementLists.new
@@ -37,12 +35,10 @@ module AppiumFailureHelper
37
35
 
38
36
  def self.load_all_from_yaml
39
37
  elements_map = {}
40
- # ALTERADO: Lê o caminho base da configuração central.
41
38
  config = AppiumFailureHelper.configuration
42
39
  glob_path = File.join(Dir.pwd, config.elements_path, '**', '*.yaml')
43
40
 
44
41
  Dir.glob(glob_path).each do |file|
45
- # ... (o resto do método continua igual) ...
46
42
  next if file.include?('reports_failure')
47
43
  begin
48
44
  data = YAML.load_file(file)
@@ -12,22 +12,51 @@ module AppiumFailureHelper
12
12
  end
13
13
 
14
14
  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
+
15
21
  FileUtils.mkdir_p(@output_folder)
16
22
  page_source = @driver.page_source
17
- platform = @driver.capabilities['platformName']&.downcase || 'unknown'
23
+ platform_value = @driver.capabilities['platformName'] || @driver.capabilities[:platformName]
24
+ platform = platform_value&.downcase || 'unknown'
25
+
26
+ @doc = Nokogiri::XML(page_source)
27
+
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 || []
36
+
37
+ similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
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)
45
+
46
+ alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
47
+ end
48
+ end
18
49
 
19
- failed_info = Analyzer.extract_failure_details(@exception)
20
50
  unified_element_map = ElementRepository.load_all
21
51
  de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
22
-
23
- page_analyzer = PageAnalyzer.new(page_source, platform)
24
- all_page_elements = page_analyzer.analyze
25
- similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements)
52
+ code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
26
53
 
27
54
  report_data = {
28
55
  failed_element: failed_info,
29
56
  similar_elements: similar_elements,
57
+ alternative_xpaths: alternative_xpaths,
30
58
  de_para_analysis: de_para_result,
59
+ code_search_results: code_search_results,
31
60
  all_page_elements: all_page_elements,
32
61
  screenshot_base64: @driver.screenshot_as(:base64),
33
62
  platform: platform,
@@ -35,10 +64,14 @@ module AppiumFailureHelper
35
64
  }
36
65
 
37
66
  ReportGenerator.new(@output_folder, page_source, report_data).generate_all
38
-
39
67
  Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
68
+
40
69
  rescue => e
41
- Utils.logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
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 "----------------------------------------"
42
75
  end
43
76
  end
44
77
  end
@@ -1,4 +1,3 @@
1
- # lib/appium_failure_helper/page_analyzer.rb
2
1
  module AppiumFailureHelper
3
2
  class PageAnalyzer
4
3
  PREFIX = {
@@ -21,19 +20,26 @@ module AppiumFailureHelper
21
20
  @platform = platform
22
21
  end
23
22
 
24
- def analyze
23
+ def analyze
25
24
  seen_elements = {}
26
25
  all_elements_suggestions = []
27
26
  @doc.xpath('//*').each do |node|
28
27
  next if ['hierarchy', 'AppiumAUT'].include?(node.name)
29
28
  attrs = node.attributes.transform_values(&:value)
30
- unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
31
- unless seen_elements[unique_key]
32
- name = suggest_name(node.name, attrs)
33
- locators = xpath_generator(node.name, attrs)
34
- all_elements_suggestions << { name: name, locators: locators }
35
- seen_elements[unique_key] = true
36
- end
29
+
30
+ unique_key = node.path
31
+ next if seen_elements[unique_key]
32
+
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
37
43
  end
38
44
  all_elements_suggestions
39
45
  end
@@ -22,127 +22,177 @@ module AppiumFailureHelper
22
22
  analysis_report = {
23
23
  failed_element: @data[:failed_element],
24
24
  similar_elements: @data[:similar_elements],
25
- de_para_yaml_analysis: @data[:de_para_yaml_analysis],
26
- de_para_rb_analysis: @data[:de_para_rb_analysis]
25
+ de_para_analysis: @data[:de_para_analysis],
26
+ code_search_results: @data[:code_search_results],
27
+ alternative_xpaths: @data[:alternative_xpaths]
27
28
  }
28
29
  File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
29
30
  File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
30
31
  end
31
32
 
32
- # --- MÉTODO CORRIGIDO COM A LÓGICA COMPLETA ---
33
33
  def generate_html_report
34
- # Prepara as variáveis a partir do hash de dados
35
- failed_info = @data[:failed_element]
36
- similar_elements = @data[:similar_elements]
37
- all_suggestions = @data[:all_page_elements]
38
- de_para_yaml = @data[:de_para_yaml_analysis]
39
- de_para_rb = @data[:de_para_rb_analysis]
34
+ failed_info = @data[:failed_element] || {}
35
+ similar_elements = @data[:similar_elements] || []
36
+ all_suggestions = @data[:all_page_elements] || []
37
+ de_para_analysis = @data[:de_para_analysis]
38
+ code_search_results = @data[:code_search_results] || []
39
+ alternative_xpaths = @data[:alternative_xpaths] || []
40
40
  timestamp = @data[:timestamp]
41
41
  platform = @data[:platform]
42
42
  screenshot_base64 = @data[:screenshot_base64]
43
43
 
44
- # CORREÇÃO: Funções (lambdas) para gerar HTML restauradas na íntegra
45
44
  locators_html = lambda do |locators|
46
- 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
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
47
49
  end
48
50
 
49
51
  all_elements_html = lambda do |elements|
50
- 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
52
+ (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
51
53
  end
54
+
55
+ de_para_html = ""
56
+ failed_info_content = ""
52
57
 
53
- # Bloco de análise YAML
54
- de_para_yaml_html = ""
55
- if de_para_yaml
56
- de_para_yaml_html = <<~HTML
57
- <div class="bg-green-50 border border-green-200 p-4 rounded-lg shadow-md mb-6">
58
- <h3 class="text-lg font-bold text-green-800 mb-2">Análise de Mapeamento YAML (.yaml)</h3>
59
- <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_yaml[:logical_name])}</strong> foi encontrado nos arquivos .yaml do projeto!</p>
60
- <p class="text-sm text-gray-700">O localizador correto definido é:</p>
61
- <div class="font-mono text-xs bg-green-100 p-2 mt-2 rounded">
62
- <span class="font-bold">#{CGI.escapeHTML(de_para_yaml[:correct_locator]['tipoBusca'].upcase)}:</span>
63
- <span class="break-all">#{CGI.escapeHTML(de_para_yaml[:correct_locator]['valor'])}</span>
58
+ code_search_html = ""
59
+ unless code_search_results.empty?
60
+ suggestions_list = code_search_results.map do |match|
61
+ score_percent = (match[:score] * 100).round(1)
62
+ <<~SUGGESTION
63
+ <div class='border border-sky-200 bg-sky-50 p-3 rounded-lg mb-2'>
64
+ <p class='text-sm text-gray-600'>Encontrado em: <strong class='font-mono'>#{match[:file]}:#{match[:line_number]}</strong></p>
65
+ <pre class='bg-gray-800 text-white p-2 rounded mt-2 text-xs overflow-auto'><code>#{CGI.escapeHTML(match[:code])}</code></pre>
66
+ <p class='text-xs text-green-600 mt-1'>Similaridade: #{score_percent}%</p>
64
67
  </div>
68
+ SUGGESTION
69
+ end.join
70
+ code_search_html = <<~HTML
71
+ <div class="bg-white p-4 rounded-lg shadow-md">
72
+ <h2 class="text-xl font-bold text-sky-700 mb-4">Sugestões Encontradas no Código</h2>
73
+ #{suggestions_list}
65
74
  </div>
66
75
  HTML
67
76
  end
68
77
 
69
- # Bloco de análise Ruby
70
- de_para_rb_html = ""
71
- if de_para_rb
72
- if de_para_rb[:found]
73
- de_para_rb_html = <<~HTML
74
- <div class="bg-green-50 border border-green-200 p-4 rounded-lg shadow-md mb-6">
75
- <h3 class="text-lg font-bold text-green-800 mb-2">Análise de Mapeamento Ruby (.rb)</h3>
76
- <p class="text-sm text-gray-700">A definição do elemento foi encontrada com sucesso no arquivo <strong class="font-mono bg-gray-200 px-1 rounded">#{de_para_rb[:path]}</strong>.</p>
78
+ failed_info_content = if failed_info && !failed_info.empty?
79
+ "<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
+ else
81
+ "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
82
+ end
83
+
84
+ repair_strategies_content = if alternative_xpaths.empty?
85
+ "<p class='text-gray-500'>Nenhuma estratégia de XPath alternativa pôde ser gerada para o elemento alvo.</p>"
86
+ else
87
+ pages = alternative_xpaths.each_slice(6).to_a
88
+
89
+ carousel_items = pages.map do |page_strategies|
90
+ strategy_list_html = page_strategies.map do |strategy|
91
+ reliability_color = case strategy[:reliability]
92
+ when :alta then 'bg-green-100 text-green-800'
93
+ when :media then 'bg-yellow-100 text-yellow-800'
94
+ else 'bg-red-100 text-red-800'
95
+ end
96
+ <<~STRATEGY_ITEM
97
+ <div class='border border-gray-200 rounded-lg p-3 bg-white'>
98
+ <div class='flex justify-between items-center mb-2'>
99
+ <p class='font-semibold text-indigo-800 text-sm'>#{CGI.escapeHTML(strategy[:name])}</p>
100
+ <span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{CGI.escapeHTML(strategy[:reliability].to_s.capitalize)}</span>
101
+ </div>
102
+ <pre class='bg-gray-800 text-white p-2 rounded text-xs whitespace-pre-wrap break-words'><code>#{CGI.escapeHTML(strategy[:locator])}</code></pre>
103
+ </div>
104
+ STRATEGY_ITEM
105
+ end.join
106
+ "<div class='carousel-item w-full flex-shrink-0'><div class='space-y-3'>#{strategy_list_html}</div></div>"
107
+ end.join
108
+
109
+ <<~CAROUSEL
110
+ <div id="xpath-carousel" class="relative">
111
+ <div class="overflow-hidden">
112
+ <div class="carousel-track flex transition-transform duration-300 ease-in-out">
113
+ #{carousel_items}
114
+ </div>
77
115
  </div>
78
- HTML
79
- else
80
- de_para_rb_html = <<~HTML
81
- <div class="bg-yellow-50 border border-yellow-200 p-4 rounded-lg shadow-md mb-6">
82
- <h3 class="text-lg font-bold text-yellow-800 mb-2">Análise de Mapeamento Ruby (.rb)</h3>
83
- <p class="text-sm text-gray-700">O elemento <strong class="font-mono bg-gray-200 px-1 rounded">#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</strong> NÃO foi encontrado no arquivo <strong class="font-mono bg-gray-200 px-1 rounded">#{de_para_rb[:path]}</strong>.</p>
84
- <p class="text-xs text-gray-500 mt-1">Motivo: #{de_para_rb[:reason]}</p>
116
+ <div class="flex items-center justify-center space-x-4 mt-4">
117
+ <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 disabled:cursor-not-allowed transition-colors">
118
+ &lt; Anterior
119
+ </button>
120
+ <div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
121
+ <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 disabled:cursor-not-allowed transition-colors">
122
+ Próximo &gt;
123
+ </button>
85
124
  </div>
86
- HTML
87
- end
125
+ </div>
126
+ CAROUSEL
88
127
  end
89
128
 
90
- # Geração do conteúdo das seções que estavam faltando
91
- similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>" : similar_elements.map { |el|
92
- score_percent = (el[:score] * 100).round(1)
93
- "<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>"
94
- }.join
95
-
96
- failed_info_content = if failed_info && failed_info[:selector_value]
97
- "<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>"
129
+ similar_elements_content = if similar_elements.empty?
130
+ "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>"
98
131
  else
99
- "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
132
+ carousel_items = similar_elements.map do |el|
133
+ score_percent = (el[:score] * 100).round(1)
134
+ <<~ITEM
135
+ <div class="carousel-item w-full flex-shrink-0">
136
+ <div class='border border-indigo-100 p-4 rounded-lg bg-indigo-50'>
137
+ <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>
138
+ <ul>#{locators_html.call(el[:locators])}</ul>
139
+ </div>
140
+ </div>
141
+ ITEM
142
+ end.join
143
+ <<~CAROUSEL
144
+ <div id="similar-elements-carousel" class="relative">
145
+ <div class="overflow-hidden rounded-lg bg-white"><div class="carousel-track flex transition-transform duration-300 ease-in-out">#{carousel_items}</div></div>
146
+ <div class="flex items-center justify-center space-x-4 mt-4">
147
+ <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>
148
+ <div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
149
+ <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>
150
+ </div>
151
+ </div>
152
+ CAROUSEL
100
153
  end
101
154
 
102
- # Template HTML completo
103
155
  html_content = <<~HTML_REPORT
104
156
  <!DOCTYPE html>
105
157
  <html lang="pt-BR">
106
158
  <head>
107
- <meta charset="UTF-8">
108
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
159
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
109
160
  <title>Relatório de Falha Appium - #{timestamp}</title>
110
161
  <script src="https://cdn.tailwindcss.com"></script>
111
- <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>
162
+ <style> .tab-button.active { border-bottom: 2px solid #4f46e5; color: #4f46e5; font-weight: 600; } .tab-content { display: none; } .tab-content.active { display: block; } </style>
112
163
  </head>
113
- <body class="bg-gray-50 p-8">
164
+ <body class="bg-gray-100 p-4 sm:p-8">
114
165
  <div class="max-w-7xl mx-auto">
115
166
  <header class="mb-8 pb-4 border-b border-gray-300">
116
167
  <h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
117
- <p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
168
+ <p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.to_s.upcase}</p>
118
169
  </header>
119
170
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
120
- <div class="lg:col-span-1">
121
- #{de_para_yaml_html}
122
- #{de_para_rb_html}
123
- <div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
171
+ <div class="lg:col-span-1 space-y-6">
172
+ <div class="bg-white p-4 rounded-lg shadow-md border border-red-200">
124
173
  <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
125
174
  #{failed_info_content}
126
175
  </div>
127
- <div class="bg-white p-4 rounded-lg shadow-xl">
176
+ #{code_search_html}
177
+ <div class="bg-white p-4 rounded-lg shadow-md">
128
178
  <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
129
179
  <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
130
180
  </div>
131
181
  </div>
132
182
  <div class="lg:col-span-2">
133
- <div class="bg-white rounded-lg shadow-xl">
183
+ <div class="bg-white rounded-lg shadow-md">
134
184
  <div class="flex border-b border-gray-200">
135
- <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>
136
- <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>
185
+ <button class="tab-button active px-4 py-3 text-sm" data-tab="strategies">Estratégias de Reparo (#{alternative_xpaths.size})</button>
186
+ <button class="tab-button px-4 py-3 text-sm text-gray-600" data-tab="all">Dump Completo (#{all_suggestions.size})</button>
137
187
  </div>
138
188
  <div class="p-6">
139
- <div id="similar" class="tab-content active">
140
- <h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
141
- <div class="space-y-4">#{similar_elements_content}</div>
189
+ <div id="strategies" class="tab-content active">
190
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Estratégias de Localização Alternativas</h3>
191
+ #{repair_strategies_content}
142
192
  </div>
143
193
  <div id="all" class="tab-content">
144
- <h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump de Todos os Elementos da Tela</h3>
145
- <div class="max-h-[600px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
194
+ <h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
195
+ <div class="max-h-[800px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
146
196
  </div>
147
197
  </div>
148
198
  </div>
@@ -150,21 +200,61 @@ module AppiumFailureHelper
150
200
  </div>
151
201
  </div>
152
202
  <script>
153
- document.querySelectorAll('.tab-button').forEach(tab => {
154
- tab.addEventListener('click', () => {
155
- const target = tab.getAttribute('data-tab');
156
- document.querySelectorAll('.tab-button').forEach(t => t.classList.remove('active'));
157
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
158
- tab.classList.add('active');
159
- document.getElementById(target).classList.add('active');
203
+ document.addEventListener('DOMContentLoaded', () => {
204
+ const tabs = document.querySelectorAll('.tab-button');
205
+ tabs.forEach(tab => {
206
+ tab.addEventListener('click', (e) => {
207
+ e.preventDefault();
208
+ const target = tab.getAttribute('data-tab');
209
+ tabs.forEach(t => t.classList.remove('active'));
210
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
211
+ tab.classList.add('active');
212
+ document.getElementById(target).classList.add('active');
213
+ });
160
214
  });
215
+
216
+ const carousel = document.getElementById('xpath-carousel');
217
+ if (carousel) {
218
+ const track = carousel.querySelector('.carousel-track');
219
+ const items = carousel.querySelectorAll('.carousel-item');
220
+ const prevButton = carousel.querySelector('.carousel-prev-footer');
221
+ const nextButton = carousel.querySelector('.carousel-next-footer');
222
+ const counter = carousel.querySelector('.carousel-counter');
223
+ const totalItems = items.length;
224
+ let currentIndex = 0;
225
+
226
+ function updateCarousel() {
227
+ if (totalItems === 0) {
228
+ if(counter) counter.textContent = "";
229
+ return;
230
+ };
231
+ track.style.transform = `translateX(-${currentIndex * 100}%)`;
232
+ if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
233
+ if (prevButton) { prevButton.disabled = currentIndex === 0; }
234
+ if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
235
+ }
236
+
237
+ if (nextButton) {
238
+ nextButton.addEventListener('click', () => {
239
+ if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
240
+ });
241
+ }
242
+
243
+ if (prevButton) {
244
+ prevButton.addEventListener('click', () => {
245
+ if (currentIndex > 0) { currentIndex--; updateCarousel(); }
246
+ });
247
+ }
248
+
249
+ if (totalItems > 0) { updateCarousel(); }
250
+ }
161
251
  });
162
252
  </script>
163
253
  </body>
164
254
  </html>
165
255
  HTML_REPORT
166
256
 
167
- File.write("#{@output_folder}/report_#{timestamp}.html", html_content)
257
+ File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
168
258
  end
169
259
  end
170
260
  end
@@ -0,0 +1,48 @@
1
+ module AppiumFailureHelper
2
+ module SourceCodeAnalyzer
3
+ 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*\(?['"]([^'"]+)['"]\)?/ }
12
+ ].freeze
13
+
14
+ def self.extract_from_exception(exception)
15
+ location = exception.backtrace.find { |line| line.include?('.rb') && !line.include?('gems') }
16
+ return {} unless location
17
+
18
+ path_match = location.match(/^(.*?):(\d+)(?::in.*)?$/)
19
+ return {} unless path_match
20
+
21
+ file_path = path_match[1]
22
+ line_number = path_match[2]
23
+
24
+ return {} unless File.exist?(file_path)
25
+
26
+ begin
27
+ error_line = File.readlines(file_path)[line_number.to_i - 1]
28
+ return parse_line_for_locator(error_line)
29
+ rescue
30
+ return {}
31
+ end
32
+ end
33
+
34
+ def self.parse_line_for_locator(line)
35
+ PATTERNS.each do |pattern_info|
36
+ match = line.match(pattern_info[:regex])
37
+ if match
38
+ return {
39
+ selector_type: pattern_info[:type].to_s,
40
+ selector_value: match[1],
41
+ analysis_method: "Análise de Código-Fonte"
42
+ }
43
+ end
44
+ end
45
+ {}
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module AppiumFailureHelper
4
- VERSION = "0.6.8"
2
+ VERSION = "1.0.0"
5
3
  end
@@ -0,0 +1,89 @@
1
+ module AppiumFailureHelper
2
+ module XPathFactory
3
+ MAX_STRATEGIES = 20
4
+
5
+ def self.generate_for_node(node)
6
+ return [] unless node
7
+ tag = node.name
8
+ attrs = node.attributes.transform_values(&:value) || {}
9
+ strategies = []
10
+
11
+ add_direct_attribute_strategies(strategies, tag, attrs)
12
+ add_combinatorial_strategies(strategies, tag, attrs)
13
+ add_parent_based_strategies(strategies, tag, node)
14
+ add_relational_strategies(strategies, node)
15
+ add_partial_text_strategies(strategies, tag, attrs)
16
+ add_boolean_strategies(strategies, tag, attrs)
17
+ add_positional_strategies(strategies, node)
18
+
19
+ strategies.uniq { |s| s[:locator] }.first(MAX_STRATEGIES)
20
+ end
21
+
22
+ private
23
+
24
+ def self.add_direct_attribute_strategies(strategies, tag, attrs)
25
+ if (id = attrs['resource-id']) && !id.empty?
26
+ strategies << { name: "ID Único (Recomendado)", strategy: 'id', locator: id, reliability: :alta }
27
+ end
28
+ if (text = attrs['text']) && !text.empty?
29
+ strategies << { name: "Texto Exato", strategy: 'xpath', locator: "//#{tag}[@text='#{text}']", reliability: :alta }
30
+ end
31
+ if (desc = attrs['content-desc']) && !desc.empty?
32
+ strategies << { name: "Content Description", strategy: 'xpath', locator: "//#{tag}[@content-desc='#{desc}']", reliability: :alta }
33
+ end
34
+ end
35
+
36
+ def self.add_combinatorial_strategies(strategies, tag, attrs)
37
+ valid_attrs = attrs.select { |k, v| %w[text content-desc class package].include?(k) && v && !v.empty? }
38
+ return if valid_attrs.keys.size < 2
39
+
40
+ valid_attrs.keys.combination(2).each do |comb|
41
+ locator_parts = comb.map { |k| "@#{k}='#{attrs[k]}'" }.join(' and ')
42
+ attr_names = comb.map(&:capitalize).join(' + ')
43
+ strategies << { name: "Combinação: #{attr_names}", strategy: 'xpath', locator: "//#{tag}[#{locator_parts}]", reliability: :alta }
44
+ end
45
+ end
46
+
47
+ def self.add_parent_based_strategies(strategies, tag, node)
48
+ parent = node.parent
49
+ return unless parent && parent.name != 'hierarchy'
50
+
51
+ parent_attrs = parent.attributes.transform_values(&:value) || {}
52
+
53
+ if (id = parent_attrs['resource-id']) && !id.empty?
54
+ strategies << { name: "Filho de Pai com ID", strategy: 'xpath', locator: "//*[@resource-id='#{id}']//#{tag}", reliability: :alta }
55
+ else
56
+ parent_attrs.each do |k, v|
57
+ next if v.empty?
58
+ strategies << { name: "Filho de Pai com #{k}", strategy: 'xpath', locator: "//*[@#{k}='#{v}']//#{tag}", reliability: :media }
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.add_relational_strategies(strategies, node)
64
+ if (prev_sibling = node.previous_sibling) && (text = prev_sibling['text']) && !text.empty?
65
+ strategies << { name: "Relativo ao Irmão Anterior", strategy: 'xpath', locator: "//#{prev_sibling.name}[@text='#{text}']/following-sibling::#{node.name}[1]", reliability: :media }
66
+ end
67
+ end
68
+
69
+ def self.add_partial_text_strategies(strategies, tag, attrs)
70
+ if (text = attrs['text']) && !text.empty? && text.split.size > 1
71
+ strategies << { name: "Texto Parcial (contains)", strategy: 'xpath', locator: "//#{tag}[contains(@text, '#{text.split.first}')]", reliability: :media }
72
+ end
73
+ end
74
+
75
+ def self.add_boolean_strategies(strategies, tag, attrs)
76
+ %w[enabled checked selected].each do |attr|
77
+ if attrs[attr] == 'true'
78
+ strategies << { name: "#{attr.capitalize} é Verdadeiro", strategy: 'xpath', locator: "//#{tag}[@#{attr}='true']", reliability: :media }
79
+ end
80
+ end
81
+ end
82
+
83
+ def self.add_positional_strategies(strategies, node)
84
+ index = node.xpath('preceding-sibling::' + node.name).count + 1
85
+ strategies << { name: "Índice na Tela (Frágil)", strategy: 'xpath', locator: "(//#{node.name})[#{index}]", reliability: :baixa }
86
+ strategies << { name: "Caminho Absoluto (Não Recomendado)", strategy: 'xpath', locator: node.path, reliability: :baixa }
87
+ end
88
+ end
89
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'nokogiri'
4
2
  require 'fileutils'
5
3
  require 'base64'
@@ -8,11 +6,13 @@ require 'logger'
8
6
  require 'did_you_mean'
9
7
  require 'cgi'
10
8
 
11
- # Carrega todos os nossos novos módulos
12
9
  require_relative 'appium_failure_helper/utils'
13
10
  require_relative 'appium_failure_helper/analyzer'
11
+ require_relative 'appium_failure_helper/source_code_analyzer'
12
+ require_relative 'appium_failure_helper/code_searcher'
14
13
  require_relative 'appium_failure_helper/element_repository'
15
14
  require_relative 'appium_failure_helper/page_analyzer'
15
+ require_relative 'appium_failure_helper/xpath_factory'
16
16
  require_relative 'appium_failure_helper/report_generator'
17
17
  require_relative 'appium_failure_helper/handler'
18
18
  require_relative 'appium_failure_helper/configuration'
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.8
4
+ version: 1.0.0
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-25 00:00:00.000000000 Z
11
+ date: 2025-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -95,13 +95,16 @@ files:
95
95
  - Rakefile
96
96
  - lib/appium_failure_helper.rb
97
97
  - lib/appium_failure_helper/analyzer.rb
98
+ - lib/appium_failure_helper/code_searcher.rb
98
99
  - lib/appium_failure_helper/configuration.rb
99
100
  - lib/appium_failure_helper/element_repository.rb
100
101
  - lib/appium_failure_helper/handler.rb
101
102
  - lib/appium_failure_helper/page_analyzer.rb
102
103
  - lib/appium_failure_helper/report_generator.rb
104
+ - lib/appium_failure_helper/source_code_analyzer.rb
103
105
  - lib/appium_failure_helper/utils.rb
104
106
  - lib/appium_failure_helper/version.rb
107
+ - lib/appium_failure_helper/xpath_factory.rb
105
108
  - sig/appium_failure_helper.rbs
106
109
  homepage: https://github.com/David-Nascimento/Appium_failure_helper
107
110
  licenses: