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 +4 -4
- data/lib/appium_failure_helper/analyzer.rb +49 -10
- data/lib/appium_failure_helper/code_searcher.rb +44 -0
- data/lib/appium_failure_helper/element_repository.rb +0 -4
- data/lib/appium_failure_helper/handler.rb +63 -27
- data/lib/appium_failure_helper/page_analyzer.rb +15 -9
- data/lib/appium_failure_helper/report_generator.rb +248 -88
- data/lib/appium_failure_helper/source_code_analyzer.rb +48 -0
- data/lib/appium_failure_helper/version.rb +1 -3
- data/lib/appium_failure_helper/xpath_factory.rb +89 -0
- data/lib/appium_failure_helper.rb +3 -3
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fbacdea5fe5540838204bcabcc355e38b455afbfe49f65d4628955d8f84f226
|
|
4
|
+
data.tar.gz: 526f397b33ea3cdd87831c35275dfbd313e9d81dd60d6c363ad717972d63f096
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
|
|
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
|
+
< 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 >
|
|
142
|
+
</button>
|
|
85
143
|
</div>
|
|
86
|
-
|
|
87
|
-
|
|
144
|
+
</div>
|
|
145
|
+
CAROUSEL
|
|
88
146
|
end
|
|
89
147
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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"> < 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 > </button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
CAROUSEL
|
|
100
172
|
end
|
|
101
173
|
|
|
102
|
-
|
|
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>
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
136
|
-
<button class="tab-button px-4 py-3 text-sm
|
|
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="
|
|
140
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">
|
|
141
|
-
|
|
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-
|
|
145
|
-
<div class="max-h-[
|
|
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
|
-
|
|
153
|
-
document.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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:
|
|
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-
|
|
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:
|