appium_failure_helper 1.1.5 → 1.1.6

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: e3ecea0ef4f0ef9f44f103bbe8c03108cbbf2200c1b3c72c27c14f41c598e463
4
- data.tar.gz: 58b26c45f12392193baf2963a829f63504db35b8acb017774a65d8fb98e15b52
3
+ metadata.gz: 78e35ec0789690a1c581b094e7f1a1bc8cf87ffd7b5775b58805bc3d0954e2c0
4
+ data.tar.gz: 62f6e3a7a2338ec85b6f5c37064509d45534cf55197e0a791048763aac85e044
5
5
  SHA512:
6
- metadata.gz: 553620c60e1306d4bbdac5340556b69712343f8736697cc1f08a278ae4d60768e098021a562424e4bf0664ed77237bd1979b3e0c3a33f652178d5bbda012bf4d
7
- data.tar.gz: 165d91270f4f0ce695d565c5c8943c5c41c8aff49337d37ea1d2e78640509b6d878d2700ed9ffa8ac6050a6aab229e85fe01a65c8de07fdb0cf2a2a83374b027
6
+ metadata.gz: 3f57d410bc16465a452693cd5c2211fcc159def18c39b58d6de140be38e37c78f64b625f31844f77e566b5c222fbcd1d5c79b64408fad427038f360f75a4a07a
7
+ data.tar.gz: e7322ab59bde3d4fa00551573ef5fadb6e8091f7b374b9170dc1fddac247908393b442be3c0b72ef3fb144e2d680547631a946d4711cea03d3ff7029d2536a42
@@ -1,30 +1,55 @@
1
1
  module AppiumFailureHelper
2
2
  module Analyzer
3
3
  def self.triage_error(exception)
4
- # Simples e direto: se for um desses erros, é um problema de seletor.
5
- if exception.is_a?(Selenium::WebDriver::Error::NoSuchElementError) || exception.is_a?(Selenium::WebDriver::Error::TimeoutError)
4
+ case exception
5
+ when Selenium::WebDriver::Error::NoSuchElementError,
6
+ Selenium::WebDriver::Error::TimeoutError,
7
+ Selenium::WebDriver::Error::UnknownCommandError
6
8
  :locator_issue
9
+ when Selenium::WebDriver::Error::ElementNotInteractableError
10
+ :visibility_issue
11
+ when Selenium::WebDriver::Error::StaleElementReferenceError
12
+ :stale_element_issue
13
+ when defined?(RSpec::Expectations::ExpectationNotMetError) ? RSpec::Expectations::ExpectationNotMetError : Class.new
14
+ :assertion_failure
15
+ when NoMethodError, NameError, ArgumentError, TypeError
16
+ :ruby_code_issue
17
+ when Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
18
+ :session_startup_issue
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
7
22
  else
8
- :generic_issue # Para todos os outros casos
23
+ :unknown_issue
9
24
  end
10
25
  end
11
26
 
12
27
  def self.extract_failure_details(exception)
13
- message = exception.message
28
+ message = exception.message.to_s
14
29
  info = {}
15
- # único padrão que precisa, para ler a mensagem enriquecida
16
- pattern = /using "([^"]+)" with value "([^"]+)"/
17
-
18
- match = message.match(pattern)
19
- if match
20
- info[:selector_type] = match.captures[0]
21
- info[:selector_value] = match.captures[1]
30
+ patterns = [
31
+ /using "([^"]+)" with value "([^"]+)"/,
32
+ /element with locator ['"]?(#?\w+)['"]?/i,
33
+ /(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
34
+ /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
35
+ ]
36
+ patterns.each do |pattern|
37
+ match = message.match(pattern)
38
+ if match
39
+ if match.captures.size == 2
40
+ info[:selector_type], info[:selector_value] = match.captures.map(&:strip)
41
+ else
42
+ info[:selector_value] = match.captures.first.strip
43
+ info[:selector_type] = 'logical_name'
44
+ end
45
+ return info
46
+ end
22
47
  end
23
48
  info
24
49
  end
25
-
50
+
26
51
  def self.find_de_para_match(failed_info, element_map)
27
- failed_value = failed_info[:selector_value].to_s
52
+ failed_value = (failed_info || {})[:selector_value].to_s
28
53
  return nil if failed_value.empty?
29
54
  logical_name_key = failed_value.gsub(/^#/, '')
30
55
  if element_map.key?(logical_name_key)
@@ -32,7 +57,7 @@ module AppiumFailureHelper
32
57
  end
33
58
  cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
34
59
  element_map.each do |name, locator_info|
35
- mapped_locator = locator_info['valor'].to_s || locator_info['value'].to_s
60
+ mapped_locator = (locator_info || {})['valor'].to_s
36
61
  cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
37
62
  distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
38
63
  max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
@@ -46,14 +71,14 @@ module AppiumFailureHelper
46
71
  end
47
72
 
48
73
  def self.find_similar_elements(failed_info, all_page_suggestions)
49
- failed_locator_value = failed_info[:selector_value]
50
- failed_locator_type = failed_info[:selector_type]
74
+ failed_locator_value = (failed_info || {})[:selector_value]
75
+ failed_locator_type = (failed_info || {})[:selector_type]
51
76
  return [] unless failed_locator_value && failed_locator_type
52
77
  normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
53
78
  cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
54
79
  similarities = []
55
80
  all_page_suggestions.each do |suggestion|
56
- candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
81
+ candidate_locator = (suggestion[:locators] || []).find { |loc| loc[:strategy] == normalized_failed_type }
57
82
  next unless candidate_locator
58
83
  cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
59
84
  distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
@@ -1,50 +1,42 @@
1
+ # lib/appium_failure_helper/element_repository.rb
1
2
  module AppiumFailureHelper
2
3
  module ElementRepository
3
-
4
4
  def self.load_all
5
- elements_map = load_from_ruby_file
6
- elements_map.merge!(load_all_from_yaml)
5
+ config = AppiumFailureHelper.configuration
6
+ base_path = config.elements_path
7
+ elements_map = load_from_ruby_file(base_path, config.elements_ruby_file)
8
+ elements_map.merge!(load_all_from_yaml(base_path))
9
+ Utils.logger.info("Mapa de elementos carregado da pasta '#{base_path}'. Total: #{elements_map.size}.")
7
10
  elements_map
8
11
  end
9
12
 
10
13
  private
11
14
 
12
- def self.load_from_ruby_file
15
+ def self.load_from_ruby_file(base_path, filename)
13
16
  map = {}
14
- config = AppiumFailureHelper.configuration
15
- file_path = File.join(Dir.pwd, config.elements_path, config.elements_ruby_file)
16
-
17
+ file_path = File.join(base_path, filename)
17
18
  return map unless File.exist?(file_path)
18
-
19
19
  begin
20
- require file_path
20
+ require File.expand_path(file_path)
21
21
  instance = OnboardingElementLists.new
22
22
  unless instance.respond_to?(:elements)
23
- Utils.logger.warn("AVISO: A classe OnboardingElementLists não expõe um `attr_reader :elements`.")
23
+ Utils.logger.warn("AVISO: A classe #{instance.class} não expõe um `attr_reader :elements`.")
24
24
  return map
25
25
  end
26
26
  instance.elements.each do |key, value|
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 }
27
+ map[key.to_s] = { 'tipoBusca' => value[0], 'valor' => value[1] }
34
28
  end
35
29
  rescue => e
36
- Utils.logger.warn("AVISO: Erro ao processar o arquivo #{file_path}: #{e.message}")
30
+ Utils.logger.warn("AVISO: Erro ao processar o arquivo Ruby #{file_path}: #{e.message}")
37
31
  end
38
-
39
32
  map
40
33
  end
41
34
 
42
- def self.load_all_from_yaml
35
+ def self.load_all_from_yaml(base_path)
43
36
  elements_map = {}
44
- config = AppiumFailureHelper.configuration
45
- glob_path = File.join(Dir.pwd, config.elements_path, '**', '*.yaml')
46
-
47
- Dir.glob(glob_path).each do |file|
37
+ glob_path = File.join(base_path, '**', '*.yaml')
38
+ files_found = Dir.glob(glob_path)
39
+ files_found.each do |file|
48
40
  next if file.include?('reports_failure')
49
41
  begin
50
42
  data = YAML.load_file(file)
@@ -1,4 +1,3 @@
1
- # lib/appium_failure_helper/handler.rb
2
1
  module AppiumFailureHelper
3
2
  class Handler
4
3
  def self.call(driver, exception)
@@ -6,7 +5,10 @@ module AppiumFailureHelper
6
5
  end
7
6
 
8
7
  def initialize(driver, exception)
9
- @driver = driver; @exception = exception; @timestamp = Time.now.strftime('%Y%m%d_%H%M%S'); @output_folder = "reports_failure/failure_#{@timestamp}"
8
+ @driver = driver
9
+ @exception = exception
10
+ @timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
11
+ @output_folder = "reports_failure/failure_#{@timestamp}"
10
12
  end
11
13
 
12
14
  def call
@@ -17,26 +19,53 @@ module AppiumFailureHelper
17
19
  end
18
20
 
19
21
  FileUtils.mkdir_p(@output_folder)
22
+
20
23
  triage_result = Analyzer.triage_error(@exception)
21
24
 
22
- report_data = { exception: @exception, triage_result: triage_result, timestamp: @timestamp, platform: @driver.capabilities[:platformName], screenshot_base64: @driver.screenshot_as(:base64) }
25
+ report_data = {
26
+ exception: @exception,
27
+ triage_result: triage_result,
28
+ timestamp: @timestamp,
29
+ platform: @driver.capabilities['platformName'] || @driver.capabilities[:platformName] || 'unknown',
30
+ screenshot_base64: @driver.screenshot_as(:base64)
31
+ }
23
32
 
24
33
  if triage_result == :locator_issue
25
34
  page_source = @driver.page_source
26
- failed_info = Analyzer.extract_failure_details(@exception)
35
+ doc = Nokogiri::XML(page_source)
27
36
 
28
- # Se a extração da mensagem falhou, geramos o relatório simples
37
+ failed_info = Analyzer.extract_failure_details(@exception) || {}
38
+ if failed_info.empty?
39
+ failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
40
+ end
41
+
29
42
  if failed_info.empty?
30
- report_data[:triage_result] = :unidentified_locator_issue
43
+ report_data[:triage_result] = :unidentified_locator_issue
31
44
  else
32
45
  page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
33
- all_page_elements = page_analyzer.analyze
34
- best_candidate_analysis = Analyzer.perform_advanced_analysis(failed_info, all_page_elements)
46
+ all_page_elements = page_analyzer.analyze || []
47
+ similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
48
+
49
+ alternative_xpaths = []
50
+ if !similar_elements.empty?
51
+ target_suggestion = similar_elements.first
52
+ if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
53
+ target_node = doc.at_xpath(target_path)
54
+ alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
55
+ end
56
+ end
57
+
58
+ unified_element_map = ElementRepository.load_all
59
+ de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
60
+ code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
35
61
 
36
62
  report_data.merge!({
37
63
  page_source: page_source,
38
64
  failed_element: failed_info,
39
- best_candidate_analysis: best_candidate_analysis,
65
+ similar_elements: similar_elements,
66
+ alternative_xpaths: alternative_xpaths,
67
+ de_para_analysis: de_para_result,
68
+ code_search_results: code_search_results,
40
69
  all_page_elements: all_page_elements
41
70
  })
42
71
  end
@@ -44,8 +73,9 @@ module AppiumFailureHelper
44
73
 
45
74
  ReportGenerator.new(@output_folder, report_data).generate_all
46
75
  Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
76
+
47
77
  rescue => e
48
- Utils.logger.error("Erro fatal na GEM: #{e.message}\n#{e.backtrace.join("\n")}")
78
+ Utils.logger.error("Erro fatal na GEM de diagnóstico: #{e.message}\n#{e.backtrace.join("\n")}")
49
79
  end
50
80
  end
51
81
  end
@@ -18,72 +18,31 @@ module AppiumFailureHelper
18
18
  File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
19
19
  end
20
20
 
21
- def generate_yaml_reports
22
- # Gera um YAML simplificado se não for um problema de seletor
21
+ def generate_yaml_reports
23
22
  analysis_report = {
24
23
  triage_result: @data[:triage_result],
25
24
  exception_class: @data[:exception].class.to_s,
26
25
  exception_message: @data[:exception].message,
27
26
  failed_element: @data[:failed_element],
28
- similar_elements: @data[:similar_elements],
29
- de_para_analysis: @data[:de_para_analysis],
30
- code_search_results: @data[:code_search_results]
27
+ best_candidate_analysis: @data[:best_candidate_analysis]
31
28
  }
32
29
  File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
33
30
 
34
- # Só gera o dump de elementos se a análise completa tiver sido feita
35
31
  if @data[:all_page_elements]
36
32
  File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
37
33
  end
38
34
  end
39
35
 
40
36
  def generate_html_report
41
- if @data[:triage_result] == :locator_issue && @data[:failed_element].empty?
42
- html_content = 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
- else
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 Inesperado",
83
- message: "Ocorreu um erro não catalogado. Verifique o stack trace para mais detalhes."
84
- )
85
- end
86
- end
37
+ html_content = case @data[:triage_result]
38
+ when :locator_issue
39
+ build_full_report
40
+ else
41
+ build_simple_diagnosis_report(
42
+ title: "Diagnóstico Rápido de Falha",
43
+ message: "A falha não foi causada por um seletor não encontrado ou a análise do seletor falhou. Verifique a mensagem de erro original e o stack trace para a causa raiz."
44
+ )
45
+ end
87
46
 
88
47
  File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
89
48
  end
@@ -1,28 +1,20 @@
1
1
  # lib/appium_failure_helper/source_code_analyzer.rb
2
2
  module AppiumFailureHelper
3
3
  module SourceCodeAnalyzer
4
- # VERSÃO 3.0: Padrões de Regex mais flexíveis que aceitam um "receptor" opcional (como $driver).
5
4
  PATTERNS = [
6
5
  { type: 'id', regex: /(?:\$driver\.)?find_element\((?:id:|:id\s*=>)\s*['"]([^'"]+)['"]\)/ },
7
6
  { 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
7
  { type: 'id', regex: /(?:\$driver\.)?\s*id\s*\(?['"]([^'"]+)['"]\)?/ },
12
8
  { type: 'xpath', regex: /(?:\$driver\.)?\s*xpath\s*\(?['"]([^'"]+)['"]\)?/ }
13
9
  ].freeze
14
10
 
15
11
  def self.extract_from_exception(exception)
16
- # Busca a primeira linha do backtrace que seja um arquivo .rb do projeto
17
12
  location = exception.backtrace.find { |line| line.include?('.rb') && !line.include?('gems') }
18
13
  return {} unless location
19
-
20
14
  path_match = location.match(/^(.*?):(\d+)(?::in.*)?$/)
21
15
  return {} unless path_match
22
-
23
16
  file_path, line_number = path_match.captures
24
17
  return {} unless File.exist?(file_path)
25
-
26
18
  begin
27
19
  error_line = File.readlines(file_path)[line_number.to_i - 1]
28
20
  return parse_line_for_locator(error_line)
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.1.5"
2
+ VERSION = "1.1.6"
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.1.5
4
+ version: 1.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento