appium_failure_helper 0.6.8 → 1.1.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: 0fbacdea5fe5540838204bcabcc355e38b455afbfe49f65d4628955d8f84f226
4
+ data.tar.gz: 526f397b33ea3cdd87831c35275dfbd313e9d81dd60d6c363ad717972d63f096
5
5
  SHA512:
6
- metadata.gz: b9c4ab54799677d260fa7939705fc074a2e9233026b696d2d4548da344b0464f8dcbbda5016a735939819e04c2b06e700ed14d23cde1c8ac55cbfb844cae9b10
7
- data.tar.gz: d076e206ca68bd47560a95162428b1dc090df4890934f535de0c68f98b17206adb7229980175c1773dd542e65ac01dd9accbf4da83104cbb68ad893c0df773e0
6
+ metadata.gz: 0274b45b6d5a796525574666e82c6bf60b8c99eb9ac269c0ae31f11c08c411f798b893cc33f4ff86d9fa493db951bd9947d7d19ef62691b29226952f9b78abde
7
+ data.tar.gz: 6fa2f563edab7b2e5b6da5de909b8e39c89438773d40e32a767191ef6423009e71797899a0393d2db0b51f5688c165d221ddd2df6c458d611913a15cdf40c241
@@ -1,5 +1,29 @@
1
+ # lib/appium_failure_helper/analyzer.rb
1
2
  module AppiumFailureHelper
2
3
  module Analyzer
4
+
5
+ def self.triage_error(exception)
6
+ case exception
7
+ when Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::TimeoutError
8
+ :locator_issue # O elemento não foi encontrado a tempo.
9
+ when Selenium::WebDriver::Error::ElementNotInteractableError
10
+ :visibility_issue # Encontrado, mas não clicável/visível.
11
+ when Selenium::WebDriver::Error::StaleElementReferenceError
12
+ :stale_element_issue # A página mudou, o elemento "envelheceu".
13
+ when RSpec::Expectations::ExpectationNotMetError
14
+ :assertion_failure # É um bug funcional, a asserção falhou.
15
+ when NoMethodError, NameError, ArgumentError, TypeError
16
+ :ruby_code_issue # Erro de sintaxe ou lógica no código de teste.
17
+ when Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
18
+ :session_startup_issue # Problema na conexão/inicialização com o Appium.
19
+ when Selenium::WebDriver::Error::WebDriverError
20
+ return :app_crash_issue if exception.message.include?('session deleted because of page crash')
21
+ :unknown_appium_issue
22
+ else
23
+ :unknown_issue
24
+ end
25
+ end
26
+
3
27
  def self.extract_failure_details(exception)
4
28
  message = exception.message
5
29
  info = {}
@@ -19,14 +43,30 @@ module AppiumFailureHelper
19
43
  end
20
44
  info
21
45
  end
22
-
46
+
23
47
  def self.find_de_para_match(failed_info, element_map)
24
- logical_name_key = failed_info[:selector_value].to_s.gsub(/^#/, '')
48
+ failed_value = failed_info[:selector_value].to_s
49
+ return nil if failed_value.empty?
50
+
51
+ logical_name_key = failed_value.gsub(/^#/, '')
52
+
25
53
  if element_map.key?(logical_name_key)
26
- return {
27
- logical_name: logical_name_key,
28
- correct_locator: element_map[logical_name_key]
29
- }
54
+ return { logical_name: logical_name_key, correct_locator: element_map[logical_name_key] }
55
+ end
56
+
57
+ cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
58
+
59
+ element_map.each do |name, locator_info|
60
+ mapped_locator = locator_info['valor'].to_s
61
+ cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
62
+ distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
63
+ max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
64
+ next if max_len.zero?
65
+ similarity_score = 1.0 - (distance.to_f / max_len)
66
+
67
+ if similarity_score > 0.85
68
+ return { logical_name: name, correct_locator: locator_info }
69
+ end
30
70
  end
31
71
  nil
32
72
  end
@@ -36,8 +76,7 @@ module AppiumFailureHelper
36
76
  failed_locator_type = failed_info[:selector_type]
37
77
  return [] unless failed_locator_value && failed_locator_type
38
78
 
39
- normalized_failed_type = failed_locator_type.downcase.include?('id') ? 'id' : failed_locator_type
40
-
79
+ normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
41
80
  cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
42
81
  similarities = []
43
82
 
@@ -51,8 +90,8 @@ module AppiumFailureHelper
51
90
  next if max_len.zero?
52
91
 
53
92
  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 }
93
+ if similarity_score > 0.85
94
+ similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score, attributes: suggestion[:attributes] }
56
95
  end
57
96
  end
58
97
  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)
@@ -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,33 +13,68 @@ module AppiumFailureHelper
12
13
  end
13
14
 
14
15
  def call
15
- FileUtils.mkdir_p(@output_folder)
16
- page_source = @driver.page_source
17
- platform = @driver.capabilities['platformName']&.downcase || 'unknown'
18
-
19
- failed_info = Analyzer.extract_failure_details(@exception)
20
- unified_element_map = ElementRepository.load_all
21
- 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)
26
-
27
- report_data = {
28
- failed_element: failed_info,
29
- similar_elements: similar_elements,
30
- de_para_analysis: de_para_result,
31
- all_page_elements: all_page_elements,
32
- screenshot_base64: @driver.screenshot_as(:base64),
33
- platform: platform,
34
- timestamp: @timestamp
35
- }
36
-
37
- ReportGenerator.new(@output_folder, page_source, report_data).generate_all
38
-
39
- Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
40
- rescue => e
41
- Utils.logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
16
+ begin
17
+ unless @driver && @driver.session_id
18
+ Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
19
+ return
20
+ end
21
+
22
+ FileUtils.mkdir_p(@output_folder)
23
+
24
+ triage_result = Analyzer.triage_error(@exception)
25
+
26
+ report_data = {
27
+ exception: @exception,
28
+ triage_result: triage_result,
29
+ timestamp: @timestamp,
30
+ platform: @driver.capabilities['platformName'] || @driver.capabilities[:platformName] || 'unknown',
31
+ screenshot_base64: @driver.screenshot_as(:base64)
32
+ }
33
+
34
+ if triage_result == :locator_issue
35
+ page_source = @driver.page_source
36
+ doc = Nokogiri::XML(page_source)
37
+
38
+ failed_info = Analyzer.extract_failure_details(@exception) || {}
39
+ if failed_info.empty?
40
+ failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
41
+ end
42
+
43
+ page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
44
+ all_page_elements = page_analyzer.analyze || []
45
+ similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
46
+
47
+ alternative_xpaths = []
48
+ if !similar_elements.empty?
49
+ target_suggestion = similar_elements.first
50
+ if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
51
+ target_node = doc.at_xpath(target_path)
52
+ alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
53
+ end
54
+ end
55
+
56
+ unified_element_map = ElementRepository.load_all
57
+ de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
58
+ code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
59
+
60
+ report_data.merge!({
61
+ page_source: page_source,
62
+ failed_element: failed_info,
63
+ similar_elements: similar_elements,
64
+ alternative_xpaths: alternative_xpaths,
65
+ de_para_analysis: de_para_result,
66
+ code_search_results: code_search_results,
67
+ all_page_elements: all_page_elements
68
+ })
69
+ end
70
+
71
+ # A chamada correta, passando apenas 2 argumentos
72
+ ReportGenerator.new(@output_folder, report_data).generate_all
73
+ Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
74
+
75
+ rescue => e
76
+ Utils.logger.error("Erro fatal na GEM de diagnóstico: #{e.message}\n#{e.backtrace.join("\n")}")
77
+ end
42
78
  end
43
79
  end
44
80
  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
@@ -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,152 +19,312 @@ 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
- de_para_yaml_analysis: @data[:de_para_yaml_analysis],
26
- de_para_rb_analysis: @data[:de_para_rb_analysis]
29
+ de_para_analysis: @data[:de_para_analysis],
30
+ code_search_results: @data[:code_search_results]
27
31
  }
28
32
  File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
29
- 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
38
+ end
39
+
40
+ def generate_html_report
41
+ html_content = case @data[:triage_result]
42
+ when :locator_issue
43
+ build_full_report
44
+ when :assertion_failure
45
+ build_simple_diagnosis_report(
46
+ title: "Falha de Asserção (Bug Funcional)",
47
+ 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."
48
+ )
49
+ when :visibility_issue
50
+ build_simple_diagnosis_report(
51
+ title: "Elemento Oculto ou Não-Interagível",
52
+ 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."
53
+ )
54
+ when :stale_element_issue
55
+ build_simple_diagnosis_report(
56
+ title: "Referência de Elemento Antiga (Stale)",
57
+ 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."
58
+ )
59
+ when :session_startup_issue
60
+ build_simple_diagnosis_report(
61
+ title: "Falha na Conexão com o Servidor Appium",
62
+ 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."
63
+ )
64
+ when :app_crash_issue
65
+ build_simple_diagnosis_report(
66
+ title: "Crash do Aplicativo",
67
+ 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)."
68
+ )
69
+ else # :ruby_code_issue, :unknown_issue
70
+ build_simple_diagnosis_report(
71
+ title: "Erro no Código de Teste",
72
+ 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."
73
+ )
74
+ end
75
+
76
+ File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
30
77
  end
31
78
 
32
- # --- MÉTODO CORRIGIDO COM A LÓGICA COMPLETA ---
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]
79
+ def build_full_report
80
+ failed_info = @data[:failed_element] || {}
81
+ similar_elements = @data[:similar_elements] || []
82
+ all_suggestions = @data[:all_page_elements] || []
83
+ de_para_analysis = @data[:de_para_analysis]
84
+ code_search_results = @data[:code_search_results] || []
85
+ alternative_xpaths = @data[:alternative_xpaths] || []
40
86
  timestamp = @data[:timestamp]
41
87
  platform = @data[:platform]
42
88
  screenshot_base64 = @data[:screenshot_base64]
43
-
44
- # CORREÇÃO: Funções (lambdas) para gerar HTML restauradas na íntegra
89
+
45
90
  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
91
+ (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
47
92
  end
48
93
 
49
94
  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
95
+ (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
96
  end
97
+
98
+ de_para_html = "" # (Sua lógica de_para_html)
99
+ code_search_html = "" # (Sua lógica code_search_html)
100
+ failed_info_content = if failed_info && !failed_info.empty?; # ... (Sua lógica failed_info_content)
101
+ else "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"; end
52
102
 
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>
64
- </div>
65
- </div>
66
- HTML
67
- end
103
+ repair_strategies_content = if alternative_xpaths.empty?
104
+ "<p class='text-gray-500'>Nenhuma estratégia de XPath alternativa pôde ser gerada para o elemento alvo.</p>"
105
+ else
106
+ pages = alternative_xpaths.each_slice(6).to_a
107
+
108
+ carousel_items = pages.map do |page_strategies|
109
+ strategy_list_html = page_strategies.map do |strategy|
110
+ reliability_color = case strategy[:reliability]
111
+ when :alta then 'bg-green-100 text-green-800'
112
+ when :media then 'bg-yellow-100 text-yellow-800'
113
+ else 'bg-red-100 text-red-800'
114
+ end
115
+ <<~STRATEGY_ITEM
116
+ <div class='border border-gray-200 rounded-lg p-3 bg-white'>
117
+ <div class='flex justify-between items-center mb-2'>
118
+ <p class='font-semibold text-indigo-800 text-sm'>#{CGI.escapeHTML(strategy[:name])}</p>
119
+ <span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{CGI.escapeHTML(strategy[:reliability].to_s.capitalize)}</span>
120
+ </div>
121
+ <pre class='bg-gray-800 text-white p-2 rounded text-xs whitespace-pre-wrap break-words'><code>#{CGI.escapeHTML(strategy[:locator])}</code></pre>
122
+ </div>
123
+ STRATEGY_ITEM
124
+ end.join
125
+ "<div class='carousel-item w-full flex-shrink-0'><div class='space-y-3'>#{strategy_list_html}</div></div>"
126
+ end.join
68
127
 
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>
128
+ <<~CAROUSEL
129
+ <div id="xpath-carousel" class="relative">
130
+ <div class="overflow-hidden">
131
+ <div class="carousel-track flex transition-transform duration-300 ease-in-out">
132
+ #{carousel_items}
133
+ </div>
77
134
  </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>
135
+ <div class="flex items-center justify-center space-x-4 mt-4">
136
+ <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">
137
+ &lt; Anterior
138
+ </button>
139
+ <div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
140
+ <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">
141
+ Próximo &gt;
142
+ </button>
85
143
  </div>
86
- HTML
87
- end
144
+ </div>
145
+ CAROUSEL
88
146
  end
89
147
 
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>"
148
+ similar_elements_content = if similar_elements.empty?
149
+ "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>"
98
150
  else
99
- "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
151
+ carousel_items = similar_elements.map do |el|
152
+ score_percent = (el[:score] * 100).round(1)
153
+ <<~ITEM
154
+ <div class="carousel-item w-full flex-shrink-0">
155
+ <div class='border border-indigo-100 p-4 rounded-lg bg-indigo-50'>
156
+ <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>
157
+ <ul>#{locators_html.call(el[:locators])}</ul>
158
+ </div>
159
+ </div>
160
+ ITEM
161
+ end.join
162
+ <<~CAROUSEL
163
+ <div id="similar-elements-carousel" class="relative">
164
+ <div class="overflow-hidden rounded-lg bg-white"><div class="carousel-track flex transition-transform duration-300 ease-in-out">#{carousel_items}</div></div>
165
+ <div class="flex items-center justify-center space-x-4 mt-4">
166
+ <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>
167
+ <div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
168
+ <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>
169
+ </div>
170
+ </div>
171
+ CAROUSEL
100
172
  end
101
173
 
102
- # Template HTML completo
103
- html_content = <<~HTML_REPORT
174
+ <<~HTML_REPORT
104
175
  <!DOCTYPE html>
105
176
  <html lang="pt-BR">
106
177
  <head>
107
- <meta charset="UTF-8">
108
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
178
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
109
179
  <title>Relatório de Falha Appium - #{timestamp}</title>
110
180
  <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>
181
+ <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
182
  </head>
113
- <body class="bg-gray-50 p-8">
183
+ <body class="bg-gray-100 p-4 sm:p-8">
114
184
  <div class="max-w-7xl mx-auto">
115
185
  <header class="mb-8 pb-4 border-b border-gray-300">
116
186
  <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>
187
+ <p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.to_s.upcase}</p>
118
188
  </header>
119
189
  <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">
190
+ <div class="lg:col-span-1 space-y-6">
191
+ <div class="bg-white p-4 rounded-lg shadow-md border border-red-200">
124
192
  <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
125
193
  #{failed_info_content}
126
194
  </div>
127
- <div class="bg-white p-4 rounded-lg shadow-xl">
195
+ #{code_search_html}
196
+ <div class="bg-white p-4 rounded-lg shadow-md">
128
197
  <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
129
198
  <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
130
199
  </div>
131
200
  </div>
132
201
  <div class="lg:col-span-2">
133
- <div class="bg-white rounded-lg shadow-xl">
202
+ <div class="bg-white rounded-lg shadow-md">
134
203
  <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>
204
+ <button class="tab-button active px-4 py-3 text-sm" data-tab="strategies">Estratégias de Reparo (#{alternative_xpaths.size})</button>
205
+ <button class="tab-button px-4 py-3 text-sm text-gray-600" data-tab="all">Dump Completo (#{all_suggestions.size})</button>
137
206
  </div>
138
207
  <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>
208
+ <div id="strategies" class="tab-content active">
209
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Estratégias de Localização Alternativas</h3>
210
+ #{repair_strategies_content}
211
+ </div>
212
+ <div id="similar" class="tab-content">
213
+ <div class="space-y-3 max-h-[700px] overflow-y-auto">#{similar_elements_content}</div>
142
214
  </div>
143
215
  <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>
216
+ <h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
217
+ <div class="max-h-[800px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
146
218
  </div>
147
219
  </div>
148
220
  </div>
149
221
  </div>
150
222
  </div>
151
223
  </div>
152
- <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');
224
+ <script>
225
+ document.addEventListener('DOMContentLoaded', () => {
226
+ const tabs = document.querySelectorAll('.tab-button');
227
+ tabs.forEach(tab => {
228
+ tab.addEventListener('click', (e) => {
229
+ e.preventDefault();
230
+ const target = tab.getAttribute('data-tab');
231
+ tabs.forEach(t => t.classList.remove('active'));
232
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
233
+ tab.classList.add('active');
234
+ document.getElementById(target).classList.add('active');
235
+ });
160
236
  });
237
+
238
+ const carousel = document.getElementById('xpath-carousel');
239
+ if (carousel) {
240
+ const track = carousel.querySelector('.carousel-track');
241
+ const items = carousel.querySelectorAll('.carousel-item');
242
+ const prevButton = carousel.querySelector('.carousel-prev-footer');
243
+ const nextButton = carousel.querySelector('.carousel-next-footer');
244
+ const counter = carousel.querySelector('.carousel-counter');
245
+ const totalItems = items.length;
246
+ let currentIndex = 0;
247
+
248
+ function updateCarousel() {
249
+ if (totalItems === 0) {
250
+ if(counter) counter.textContent = "";
251
+ return;
252
+ };
253
+ track.style.transform = `translateX(-${currentIndex * 100}%)`;
254
+ if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
255
+ if (prevButton) { prevButton.disabled = currentIndex === 0; }
256
+ if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
257
+ }
258
+
259
+ if (nextButton) {
260
+ nextButton.addEventListener('click', () => {
261
+ if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
262
+ });
263
+ }
264
+
265
+ if (prevButton) {
266
+ prevButton.addEventListener('click', () => {
267
+ if (currentIndex > 0) { currentIndex--; updateCarousel(); }
268
+ });
269
+ }
270
+
271
+ if (totalItems > 0) { updateCarousel(); }
272
+ }
161
273
  });
162
274
  </script>
163
275
  </body>
164
276
  </html>
165
277
  HTML_REPORT
278
+ end
166
279
 
167
- File.write("#{@output_folder}/report_#{timestamp}.html", html_content)
280
+ def build_simple_diagnosis_report(title:, message:)
281
+ exception = @data[:exception]
282
+ error_message_html = CGI.escapeHTML(exception.message.to_s)
283
+ backtrace_html = CGI.escapeHTML(exception.backtrace.join("\n"))
284
+
285
+ <<~HTML_REPORT
286
+ <!DOCTYPE html>
287
+ <html lang="pt-BR">
288
+ <head>
289
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
290
+ <title>Diagnóstico de Falha - #{title}</title>
291
+ <script src="https://cdn.tailwindcss.com"></script>
292
+ </head>
293
+ <body class="bg-gray-100 p-4 sm:p-8">
294
+ <div class="max-w-4xl mx-auto">
295
+ <header class="mb-8 pb-4 border-b border-gray-200">
296
+ <h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
297
+ <p class="text-sm text-gray-500">Relatório gerado em: #{@data[:timestamp]} | Plataforma: #{@data[:platform].to_s.upcase}</p>
298
+ </header>
299
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
300
+ <div class="md:col-span-1">
301
+ <div class="bg-white p-4 rounded-lg shadow-md">
302
+ <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
303
+ <img src="data:image/png;base64,#{@data[:screenshot_base64]}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
304
+ </div>
305
+ </div>
306
+ <div class="md:col-span-2 space-y-6">
307
+ <div class="bg-white p-6 rounded-lg shadow-md">
308
+ <h2 class="text-xl font-bold text-red-600 mb-4">Diagnóstico: #{title}</h2>
309
+ <div class="bg-red-50 border-l-4 border-red-500 text-red-800 p-4 rounded-r-lg">
310
+ <p class="font-semibold">Causa Provável:</p>
311
+ <p>#{message}</p>
312
+ </div>
313
+ </div>
314
+ <div class="bg-white p-6 rounded-lg shadow-md">
315
+ <h3 class="text-lg font-semibold text-gray-700 mb-2">Mensagem de Erro Original</h3>
316
+ <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>
317
+ </div>
318
+ <div class="bg-white p-6 rounded-lg shadow-md">
319
+ <h3 class="text-lg font-semibold text-gray-700 mb-2">Stack Trace</h3>
320
+ <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>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </body>
326
+ </html>
327
+ HTML_REPORT
168
328
  end
169
329
  end
170
330
  end
@@ -0,0 +1,48 @@
1
+ # lib/appium_failure_helper/source_code_analyzer.rb
2
+ module AppiumFailureHelper
3
+ module SourceCodeAnalyzer
4
+ # VERSÃO 3.0: Padrões de Regex mais flexíveis que aceitam um "receptor" opcional (como $driver).
5
+ PATTERNS = [
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*\(?['"]([^'"]+)['"]\)?/ }
13
+ ].freeze
14
+
15
+ def self.extract_from_exception(exception)
16
+ # Busca a primeira linha do backtrace que seja um arquivo .rb do projeto
17
+ location = exception.backtrace.find { |line| line.include?('.rb') && !line.include?('gems') }
18
+ return {} unless location
19
+
20
+ path_match = location.match(/^(.*?):(\d+)(?::in.*)?$/)
21
+ return {} unless path_match
22
+
23
+ file_path, line_number = path_match.captures
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.1.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.1.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: