appium_failure_helper 1.1.5 → 1.1.7
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 +4 -4
- data/lib/appium_failure_helper/analyzer.rb +42 -17
- data/lib/appium_failure_helper/element_repository.rb +16 -24
- data/lib/appium_failure_helper/handler.rb +40 -10
- data/lib/appium_failure_helper/report_generator.rb +28 -62
- data/lib/appium_failure_helper/source_code_analyzer.rb +0 -8
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a20ff6bfd04e9343504f2718b569ff4872a4e8a398d9dd9952f4da57c70b4395
|
|
4
|
+
data.tar.gz: 3cf66ebae97ae64bd6dce35b35279834fa86f31c46d00c6a5df7948e990a6ea1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96ea0be769224f562be575f06fdb0a75ceeb53379c1a0b587fb7085404e06c8315e4ebb81e08a8e53d5f2ee7c18c4a08b7e5f91de421520f62ec780a4f0c2b16
|
|
7
|
+
data.tar.gz: e95f63b9cf6f5079da73a32d10260f319960b689e3abc0691282b8f830cb8abc4e69be0e2d2c14008d9846a6bb719529cc6b46a8da7e0d9f1b080c1ee07a2ee9
|
|
@@ -1,30 +1,55 @@
|
|
|
1
1
|
module AppiumFailureHelper
|
|
2
2
|
module Analyzer
|
|
3
3
|
def self.triage_error(exception)
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
35
|
+
doc = Nokogiri::XML(page_source)
|
|
27
36
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
@@ -109,8 +68,12 @@ module AppiumFailureHelper
|
|
|
109
68
|
|
|
110
69
|
de_para_html = "" # (Sua lógica de_para_html)
|
|
111
70
|
code_search_html = "" # (Sua lógica code_search_html)
|
|
112
|
-
failed_info_content = if failed_info && !failed_info.empty
|
|
113
|
-
|
|
71
|
+
failed_info_content = if failed_info && !failed_info.empty?
|
|
72
|
+
"<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>"
|
|
73
|
+
else
|
|
74
|
+
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
|
|
75
|
+
end
|
|
76
|
+
|
|
114
77
|
code_search_html = ""
|
|
115
78
|
unless code_search_results.empty?
|
|
116
79
|
suggestions_list = code_search_results.map do |match|
|
|
@@ -138,11 +101,10 @@ module AppiumFailureHelper
|
|
|
138
101
|
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
|
|
139
102
|
end
|
|
140
103
|
|
|
141
|
-
|
|
142
|
-
"<p class='text-gray-500'>Nenhuma estratégia de
|
|
104
|
+
repair_suggestions_content = if alternative_xpaths.empty?
|
|
105
|
+
"<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>"
|
|
143
106
|
else
|
|
144
107
|
pages = alternative_xpaths.each_slice(6).to_a
|
|
145
|
-
|
|
146
108
|
carousel_items = pages.map do |page_strategies|
|
|
147
109
|
strategy_list_html = page_strategies.map do |strategy|
|
|
148
110
|
reliability_color = case strategy[:reliability]
|
|
@@ -150,17 +112,21 @@ module AppiumFailureHelper
|
|
|
150
112
|
when :media then 'bg-yellow-100 text-yellow-800'
|
|
151
113
|
else 'bg-red-100 text-red-800'
|
|
152
114
|
end
|
|
115
|
+
# CORREÇÃO: Adiciona o tipo de estratégia (ID, XPATH) ao lado do seletor
|
|
153
116
|
<<~STRATEGY_ITEM
|
|
154
|
-
<
|
|
155
|
-
<div class='flex justify-between items-center mb-
|
|
117
|
+
<li class='border-b border-gray-200 py-3 last:border-b-0'>
|
|
118
|
+
<div class='flex justify-between items-center mb-1'>
|
|
156
119
|
<p class='font-semibold text-indigo-800 text-sm'>#{CGI.escapeHTML(strategy[:name])}</p>
|
|
157
120
|
<span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{CGI.escapeHTML(strategy[:reliability].to_s.capitalize)}</span>
|
|
158
121
|
</div>
|
|
159
|
-
<
|
|
160
|
-
|
|
122
|
+
<div class='bg-gray-800 text-white p-2 rounded mt-1 text-xs whitespace-pre-wrap break-words font-mono'>
|
|
123
|
+
<span class='font-bold text-indigo-400'>#{CGI.escapeHTML(strategy[:strategy].to_s.upcase)}:</span>
|
|
124
|
+
<code class='ml-1'>#{CGI.escapeHTML(strategy[:locator])}</code>
|
|
125
|
+
</div>
|
|
126
|
+
</li>
|
|
161
127
|
STRATEGY_ITEM
|
|
162
128
|
end.join
|
|
163
|
-
"<div class='carousel-item w-full flex-shrink-0'><
|
|
129
|
+
"<div class='carousel-item w-full flex-shrink-0'><ul>#{strategy_list_html}</ul></div>"
|
|
164
130
|
end.join
|
|
165
131
|
|
|
166
132
|
<<~CAROUSEL
|
|
@@ -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)
|