appium_failure_helper 1.1.4 → 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: 422c6af375ea6308ceaadf40036c7e1ff5187ed660830c4a403abcea94553551
4
- data.tar.gz: 19ffb9d6d32d70274087584bce03005694fc75c6ea17d1abf2cc4c938c448d25
3
+ metadata.gz: 78e35ec0789690a1c581b094e7f1a1bc8cf87ffd7b5775b58805bc3d0954e2c0
4
+ data.tar.gz: 62f6e3a7a2338ec85b6f5c37064509d45534cf55197e0a791048763aac85e044
5
5
  SHA512:
6
- metadata.gz: d0ed3e523bcf4ee55edf522c4a93dbca4d1c60be1654ab87fbb625bce741dbb9df9eadd2458f33d6b9df8405cf9a8a775431982d46bb1a8d45b40983b995657b
7
- data.tar.gz: b903344bf3dbc2176f3cf21c217082e8c5a739bd7e2ff8cd9a7dda4a7b066a0a75cd74ead27e7a4a4b7a6fe557ba86f02aa6e8fdd1fd633af8b2e31396b45999
6
+ metadata.gz: 3f57d410bc16465a452693cd5c2211fcc159def18c39b58d6de140be38e37c78f64b625f31844f77e566b5c222fbcd1d5c79b64408fad427038f360f75a4a07a
7
+ data.tar.gz: e7322ab59bde3d4fa00551573ef5fadb6e8091f7b374b9170dc1fddac247908393b442be3c0b72ef3fb144e2d680547631a946d4711cea03d3ff7029d2536a42
@@ -1,8 +1,10 @@
1
1
  module AppiumFailureHelper
2
2
  module Analyzer
3
- def self.triage_error(exception)
3
+ def self.triage_error(exception)
4
4
  case exception
5
- when Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::TimeoutError
5
+ when Selenium::WebDriver::Error::NoSuchElementError,
6
+ Selenium::WebDriver::Error::TimeoutError,
7
+ Selenium::WebDriver::Error::UnknownCommandError
6
8
  :locator_issue
7
9
  when Selenium::WebDriver::Error::ElementNotInteractableError
8
10
  :visibility_issue
@@ -23,33 +25,31 @@ module AppiumFailureHelper
23
25
  end
24
26
 
25
27
  def self.extract_failure_details(exception)
26
- message = exception.message
28
+ message = exception.message.to_s
27
29
  info = {}
28
30
  patterns = [
31
+ /using "([^"]+)" with value "([^"]+)"/,
29
32
  /element with locator ['"]?(#?\w+)['"]?/i,
30
33
  /(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
31
34
  /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
32
- /(?:with the resource-id|with the accessibility-id) ['"]?(.+?)['"]?/i,
33
- /using "([^"]+)" with value "([^"]+)"/
34
35
  ]
35
36
  patterns.each do |pattern|
36
- match = message.match(pattern)
37
- if match
38
- if match.captures.size == 2
39
- info[:selector_type] = match.captures[0].strip.gsub(/['"]/, '')
40
- info[:selector_value] = match.captures[1].strip.gsub(/['"]/, '')
41
- else
42
- info[:selector_value] = match.captures.last.strip.gsub(/['"]/, '')
43
- info[:selector_type] = 'id' # Padrão para regex com 1 captura
44
- end
45
- return info
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'
46
44
  end
45
+ return info
46
+ end
47
47
  end
48
48
  info
49
49
  end
50
-
50
+
51
51
  def self.find_de_para_match(failed_info, element_map)
52
- failed_value = failed_info[:selector_value].to_s
52
+ failed_value = (failed_info || {})[:selector_value].to_s
53
53
  return nil if failed_value.empty?
54
54
  logical_name_key = failed_value.gsub(/^#/, '')
55
55
  if element_map.key?(logical_name_key)
@@ -57,7 +57,7 @@ module AppiumFailureHelper
57
57
  end
58
58
  cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
59
59
  element_map.each do |name, locator_info|
60
- mapped_locator = locator_info['valor'].to_s || locator_info['value'].to_s
60
+ mapped_locator = (locator_info || {})['valor'].to_s
61
61
  cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
62
62
  distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
63
63
  max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
@@ -71,14 +71,14 @@ module AppiumFailureHelper
71
71
  end
72
72
 
73
73
  def self.find_similar_elements(failed_info, all_page_suggestions)
74
- failed_locator_value = failed_info[:selector_value]
75
- failed_locator_type = failed_info[:selector_type]
74
+ failed_locator_value = (failed_info || {})[:selector_value]
75
+ failed_locator_type = (failed_info || {})[:selector_type]
76
76
  return [] unless failed_locator_value && failed_locator_type
77
77
  normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
78
78
  cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
79
79
  similarities = []
80
80
  all_page_suggestions.each do |suggestion|
81
- candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
81
+ candidate_locator = (suggestion[:locators] || []).find { |loc| loc[:strategy] == normalized_failed_type }
82
82
  next unless candidate_locator
83
83
  cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
84
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)
@@ -7,7 +7,7 @@ module AppiumFailureHelper
7
7
  def initialize(driver, exception)
8
8
  @driver = driver
9
9
  @exception = exception
10
- @timestamp = Time.now.strftime('%Ym%d_%H%M%S')
10
+ @timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
11
11
  @output_folder = "reports_failure/failure_#{@timestamp}"
12
12
  end
13
13
 
@@ -30,44 +30,45 @@ module AppiumFailureHelper
30
30
  screenshot_base64: @driver.screenshot_as(:base64)
31
31
  }
32
32
 
33
- # A análise profunda agora é executada para QUALQUER :locator_issue.
34
33
  if triage_result == :locator_issue
35
34
  page_source = @driver.page_source
36
35
  doc = Nokogiri::XML(page_source)
37
36
 
38
- # Tenta Plano A e Plano B sem reclassificar o erro.
39
37
  failed_info = Analyzer.extract_failure_details(@exception) || {}
40
38
  if failed_info.empty?
41
39
  failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
42
40
  end
43
41
 
44
- page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
45
- all_page_elements = page_analyzer.analyze || []
46
- similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
47
-
48
- alternative_xpaths = []
49
- if !similar_elements.empty?
50
- target_suggestion = similar_elements.first
51
- if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
52
- target_node = doc.at_xpath(target_path)
53
- alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
42
+ if failed_info.empty?
43
+ report_data[:triage_result] = :unidentified_locator_issue
44
+ else
45
+ page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
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
54
56
  end
55
- end
56
57
 
57
- unified_element_map = ElementRepository.load_all
58
- de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
59
- code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
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) || []
60
61
 
61
- # Adiciona os dados (mesmo que vazios) ao pacote.
62
- report_data.merge!({
63
- page_source: page_source,
64
- failed_element: failed_info,
65
- similar_elements: similar_elements,
66
- alternative_xpaths: alternative_xpaths,
67
- de_para_analysis: de_para_result,
68
- code_search_results: code_search_results,
69
- all_page_elements: all_page_elements
70
- })
62
+ report_data.merge!({
63
+ page_source: page_source,
64
+ failed_element: failed_info,
65
+ similar_elements: similar_elements,
66
+ alternative_xpaths: alternative_xpaths,
67
+ de_para_analysis: de_para_result,
68
+ code_search_results: code_search_results,
69
+ all_page_elements: all_page_elements
70
+ })
71
+ end
71
72
  end
72
73
 
73
74
  ReportGenerator.new(@output_folder, report_data).generate_all
@@ -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.4"
2
+ VERSION = "1.1.6"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appium_failure_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.6
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-30 00:00:00.000000000 Z
11
+ date: 2025-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri