appium_failure_helper 0.6.8 → 1.0.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 +26 -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 +41 -8
- data/lib/appium_failure_helper/page_analyzer.rb +15 -9
- data/lib/appium_failure_helper/report_generator.rb +166 -76
- 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: fcada9d17369c2f0afdc27bab51a796c6abd060557783155fd49863bec12f10a
|
4
|
+
data.tar.gz: 0f3cd2fbc47011c5c943ac8e9e573f62c5c15cd7727ef1b9ff1837f9db8050c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f93b8cb2b8acf629cad21a7de037d793f0a6a9415a674a110e407c13a56914570b5ddb59e457adaf79869b0903ce5d102cc56c97094207e5c476086b20241b71
|
7
|
+
data.tar.gz: b7f6b505d3864fb3079bd6a7985ed0dd02756861d743abde308c6e78928c86de10a80341f17379fdab10a5bb57d31db0375860636b83470cb91147f0ed8791ab
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# lib/appium_failure_helper/analyzer.rb
|
1
2
|
module AppiumFailureHelper
|
2
3
|
module Analyzer
|
3
4
|
def self.extract_failure_details(exception)
|
@@ -19,14 +20,30 @@ module AppiumFailureHelper
|
|
19
20
|
end
|
20
21
|
info
|
21
22
|
end
|
22
|
-
|
23
|
+
|
23
24
|
def self.find_de_para_match(failed_info, element_map)
|
24
|
-
|
25
|
+
failed_value = failed_info[:selector_value].to_s
|
26
|
+
return nil if failed_value.empty?
|
27
|
+
|
28
|
+
logical_name_key = failed_value.gsub(/^#/, '')
|
29
|
+
|
25
30
|
if element_map.key?(logical_name_key)
|
26
|
-
return {
|
27
|
-
|
28
|
-
|
29
|
-
|
31
|
+
return { logical_name: logical_name_key, correct_locator: element_map[logical_name_key] }
|
32
|
+
end
|
33
|
+
|
34
|
+
cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
35
|
+
|
36
|
+
element_map.each do |name, locator_info|
|
37
|
+
mapped_locator = locator_info['valor'].to_s
|
38
|
+
cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
39
|
+
distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
|
40
|
+
max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
|
41
|
+
next if max_len.zero?
|
42
|
+
similarity_score = 1.0 - (distance.to_f / max_len)
|
43
|
+
|
44
|
+
if similarity_score > 0.85
|
45
|
+
return { logical_name: name, correct_locator: locator_info }
|
46
|
+
end
|
30
47
|
end
|
31
48
|
nil
|
32
49
|
end
|
@@ -36,8 +53,7 @@ module AppiumFailureHelper
|
|
36
53
|
failed_locator_type = failed_info[:selector_type]
|
37
54
|
return [] unless failed_locator_value && failed_locator_type
|
38
55
|
|
39
|
-
normalized_failed_type = failed_locator_type.downcase.include?('id') ? 'id' : failed_locator_type
|
40
|
-
|
56
|
+
normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
|
41
57
|
cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
42
58
|
similarities = []
|
43
59
|
|
@@ -51,8 +67,8 @@ module AppiumFailureHelper
|
|
51
67
|
next if max_len.zero?
|
52
68
|
|
53
69
|
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 }
|
70
|
+
if similarity_score > 0.85
|
71
|
+
similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score, attributes: suggestion[:attributes] }
|
56
72
|
end
|
57
73
|
end
|
58
74
|
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)
|
@@ -12,22 +12,51 @@ module AppiumFailureHelper
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def call
|
15
|
+
unless @driver && @driver.session_id
|
16
|
+
Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
|
17
|
+
Utils.logger.error("Exceção original: #{@exception.message}")
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
15
21
|
FileUtils.mkdir_p(@output_folder)
|
16
22
|
page_source = @driver.page_source
|
17
|
-
|
23
|
+
platform_value = @driver.capabilities['platformName'] || @driver.capabilities[:platformName]
|
24
|
+
platform = platform_value&.downcase || 'unknown'
|
25
|
+
|
26
|
+
@doc = Nokogiri::XML(page_source)
|
27
|
+
|
28
|
+
failed_info = Analyzer.extract_failure_details(@exception) || {}
|
29
|
+
if failed_info.empty?
|
30
|
+
Utils.logger.info("Análise da mensagem de erro falhou. Tentando analisar código-fonte...")
|
31
|
+
failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
page_analyzer = PageAnalyzer.new(page_source, platform)
|
35
|
+
all_page_elements = page_analyzer.analyze || []
|
36
|
+
|
37
|
+
similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
|
38
|
+
|
39
|
+
alternative_xpaths = []
|
40
|
+
if !similar_elements.empty?
|
41
|
+
target_suggestion = similar_elements.first
|
42
|
+
|
43
|
+
if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
|
44
|
+
target_node = @doc.at_xpath(target_path)
|
45
|
+
|
46
|
+
alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
|
47
|
+
end
|
48
|
+
end
|
18
49
|
|
19
|
-
failed_info = Analyzer.extract_failure_details(@exception)
|
20
50
|
unified_element_map = ElementRepository.load_all
|
21
51
|
de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
|
22
|
-
|
23
|
-
page_analyzer = PageAnalyzer.new(page_source, platform)
|
24
|
-
all_page_elements = page_analyzer.analyze
|
25
|
-
similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements)
|
52
|
+
code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
|
26
53
|
|
27
54
|
report_data = {
|
28
55
|
failed_element: failed_info,
|
29
56
|
similar_elements: similar_elements,
|
57
|
+
alternative_xpaths: alternative_xpaths,
|
30
58
|
de_para_analysis: de_para_result,
|
59
|
+
code_search_results: code_search_results,
|
31
60
|
all_page_elements: all_page_elements,
|
32
61
|
screenshot_base64: @driver.screenshot_as(:base64),
|
33
62
|
platform: platform,
|
@@ -35,10 +64,14 @@ module AppiumFailureHelper
|
|
35
64
|
}
|
36
65
|
|
37
66
|
ReportGenerator.new(@output_folder, page_source, report_data).generate_all
|
38
|
-
|
39
67
|
Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
|
68
|
+
|
40
69
|
rescue => e
|
41
|
-
|
70
|
+
puts "--- ERRO FATAL NA GEM (DIAGNÓSTICO) ---"
|
71
|
+
puts "CLASSE DO ERRO: #{e.class}"
|
72
|
+
puts "MENSAGEM: #{e.message}"
|
73
|
+
puts "BACKTRACE:\n#{e.backtrace.join("\n")}"
|
74
|
+
puts "----------------------------------------"
|
42
75
|
end
|
43
76
|
end
|
44
77
|
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
|
@@ -22,127 +22,177 @@ module AppiumFailureHelper
|
|
22
22
|
analysis_report = {
|
23
23
|
failed_element: @data[:failed_element],
|
24
24
|
similar_elements: @data[:similar_elements],
|
25
|
-
|
26
|
-
|
25
|
+
de_para_analysis: @data[:de_para_analysis],
|
26
|
+
code_search_results: @data[:code_search_results],
|
27
|
+
alternative_xpaths: @data[:alternative_xpaths]
|
27
28
|
}
|
28
29
|
File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
|
29
30
|
File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
|
30
31
|
end
|
31
32
|
|
32
|
-
# --- MÉTODO CORRIGIDO COM A LÓGICA COMPLETA ---
|
33
33
|
def generate_html_report
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
failed_info = @data[:failed_element] || {}
|
35
|
+
similar_elements = @data[:similar_elements] || []
|
36
|
+
all_suggestions = @data[:all_page_elements] || []
|
37
|
+
de_para_analysis = @data[:de_para_analysis]
|
38
|
+
code_search_results = @data[:code_search_results] || []
|
39
|
+
alternative_xpaths = @data[:alternative_xpaths] || []
|
40
40
|
timestamp = @data[:timestamp]
|
41
41
|
platform = @data[:platform]
|
42
42
|
screenshot_base64 = @data[:screenshot_base64]
|
43
43
|
|
44
|
-
# CORREÇÃO: Funções (lambdas) para gerar HTML restauradas na íntegra
|
45
44
|
locators_html = lambda do |locators|
|
46
|
-
locators
|
45
|
+
(locators || []).map do |loc|
|
46
|
+
strategy_text = loc[:strategy].to_s.upcase.gsub('_', ' ')
|
47
|
+
"<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(strategy_text)}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>"
|
48
|
+
end.join
|
47
49
|
end
|
48
50
|
|
49
51
|
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
|
52
|
+
(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
53
|
end
|
54
|
+
|
55
|
+
de_para_html = ""
|
56
|
+
failed_info_content = ""
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
<
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
<span class="font-bold">#{CGI.escapeHTML(de_para_yaml[:correct_locator]['tipoBusca'].upcase)}:</span>
|
63
|
-
<span class="break-all">#{CGI.escapeHTML(de_para_yaml[:correct_locator]['valor'])}</span>
|
58
|
+
code_search_html = ""
|
59
|
+
unless code_search_results.empty?
|
60
|
+
suggestions_list = code_search_results.map do |match|
|
61
|
+
score_percent = (match[:score] * 100).round(1)
|
62
|
+
<<~SUGGESTION
|
63
|
+
<div class='border border-sky-200 bg-sky-50 p-3 rounded-lg mb-2'>
|
64
|
+
<p class='text-sm text-gray-600'>Encontrado em: <strong class='font-mono'>#{match[:file]}:#{match[:line_number]}</strong></p>
|
65
|
+
<pre class='bg-gray-800 text-white p-2 rounded mt-2 text-xs overflow-auto'><code>#{CGI.escapeHTML(match[:code])}</code></pre>
|
66
|
+
<p class='text-xs text-green-600 mt-1'>Similaridade: #{score_percent}%</p>
|
64
67
|
</div>
|
68
|
+
SUGGESTION
|
69
|
+
end.join
|
70
|
+
code_search_html = <<~HTML
|
71
|
+
<div class="bg-white p-4 rounded-lg shadow-md">
|
72
|
+
<h2 class="text-xl font-bold text-sky-700 mb-4">Sugestões Encontradas no Código</h2>
|
73
|
+
#{suggestions_list}
|
65
74
|
</div>
|
66
75
|
HTML
|
67
76
|
end
|
68
77
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
78
|
+
failed_info_content = if failed_info && !failed_info.empty?
|
79
|
+
"<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>"
|
80
|
+
else
|
81
|
+
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
|
82
|
+
end
|
83
|
+
|
84
|
+
repair_strategies_content = if alternative_xpaths.empty?
|
85
|
+
"<p class='text-gray-500'>Nenhuma estratégia de XPath alternativa pôde ser gerada para o elemento alvo.</p>"
|
86
|
+
else
|
87
|
+
pages = alternative_xpaths.each_slice(6).to_a
|
88
|
+
|
89
|
+
carousel_items = pages.map do |page_strategies|
|
90
|
+
strategy_list_html = page_strategies.map do |strategy|
|
91
|
+
reliability_color = case strategy[:reliability]
|
92
|
+
when :alta then 'bg-green-100 text-green-800'
|
93
|
+
when :media then 'bg-yellow-100 text-yellow-800'
|
94
|
+
else 'bg-red-100 text-red-800'
|
95
|
+
end
|
96
|
+
<<~STRATEGY_ITEM
|
97
|
+
<div class='border border-gray-200 rounded-lg p-3 bg-white'>
|
98
|
+
<div class='flex justify-between items-center mb-2'>
|
99
|
+
<p class='font-semibold text-indigo-800 text-sm'>#{CGI.escapeHTML(strategy[:name])}</p>
|
100
|
+
<span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{CGI.escapeHTML(strategy[:reliability].to_s.capitalize)}</span>
|
101
|
+
</div>
|
102
|
+
<pre class='bg-gray-800 text-white p-2 rounded text-xs whitespace-pre-wrap break-words'><code>#{CGI.escapeHTML(strategy[:locator])}</code></pre>
|
103
|
+
</div>
|
104
|
+
STRATEGY_ITEM
|
105
|
+
end.join
|
106
|
+
"<div class='carousel-item w-full flex-shrink-0'><div class='space-y-3'>#{strategy_list_html}</div></div>"
|
107
|
+
end.join
|
108
|
+
|
109
|
+
<<~CAROUSEL
|
110
|
+
<div id="xpath-carousel" class="relative">
|
111
|
+
<div class="overflow-hidden">
|
112
|
+
<div class="carousel-track flex transition-transform duration-300 ease-in-out">
|
113
|
+
#{carousel_items}
|
114
|
+
</div>
|
77
115
|
</div>
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
<
|
83
|
-
<
|
84
|
-
|
116
|
+
<div class="flex items-center justify-center space-x-4 mt-4">
|
117
|
+
<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">
|
118
|
+
< Anterior
|
119
|
+
</button>
|
120
|
+
<div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
|
121
|
+
<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">
|
122
|
+
Próximo >
|
123
|
+
</button>
|
85
124
|
</div>
|
86
|
-
|
87
|
-
|
125
|
+
</div>
|
126
|
+
CAROUSEL
|
88
127
|
end
|
89
128
|
|
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>"
|
129
|
+
similar_elements_content = if similar_elements.empty?
|
130
|
+
"<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>"
|
98
131
|
else
|
99
|
-
|
132
|
+
carousel_items = similar_elements.map do |el|
|
133
|
+
score_percent = (el[:score] * 100).round(1)
|
134
|
+
<<~ITEM
|
135
|
+
<div class="carousel-item w-full flex-shrink-0">
|
136
|
+
<div class='border border-indigo-100 p-4 rounded-lg bg-indigo-50'>
|
137
|
+
<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>
|
138
|
+
<ul>#{locators_html.call(el[:locators])}</ul>
|
139
|
+
</div>
|
140
|
+
</div>
|
141
|
+
ITEM
|
142
|
+
end.join
|
143
|
+
<<~CAROUSEL
|
144
|
+
<div id="similar-elements-carousel" class="relative">
|
145
|
+
<div class="overflow-hidden rounded-lg bg-white"><div class="carousel-track flex transition-transform duration-300 ease-in-out">#{carousel_items}</div></div>
|
146
|
+
<div class="flex items-center justify-center space-x-4 mt-4">
|
147
|
+
<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>
|
148
|
+
<div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
|
149
|
+
<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>
|
150
|
+
</div>
|
151
|
+
</div>
|
152
|
+
CAROUSEL
|
100
153
|
end
|
101
154
|
|
102
|
-
# Template HTML completo
|
103
155
|
html_content = <<~HTML_REPORT
|
104
156
|
<!DOCTYPE html>
|
105
157
|
<html lang="pt-BR">
|
106
158
|
<head>
|
107
|
-
<meta charset="UTF-8">
|
108
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
159
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
109
160
|
<title>Relatório de Falha Appium - #{timestamp}</title>
|
110
161
|
<script src="https://cdn.tailwindcss.com"></script>
|
111
|
-
<style>
|
162
|
+
<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
163
|
</head>
|
113
|
-
<body class="bg-gray-
|
164
|
+
<body class="bg-gray-100 p-4 sm:p-8">
|
114
165
|
<div class="max-w-7xl mx-auto">
|
115
166
|
<header class="mb-8 pb-4 border-b border-gray-300">
|
116
167
|
<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>
|
168
|
+
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.to_s.upcase}</p>
|
118
169
|
</header>
|
119
170
|
<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">
|
171
|
+
<div class="lg:col-span-1 space-y-6">
|
172
|
+
<div class="bg-white p-4 rounded-lg shadow-md border border-red-200">
|
124
173
|
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
125
174
|
#{failed_info_content}
|
126
175
|
</div>
|
127
|
-
|
176
|
+
#{code_search_html}
|
177
|
+
<div class="bg-white p-4 rounded-lg shadow-md">
|
128
178
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
129
179
|
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
130
180
|
</div>
|
131
181
|
</div>
|
132
182
|
<div class="lg:col-span-2">
|
133
|
-
<div class="bg-white rounded-lg shadow-
|
183
|
+
<div class="bg-white rounded-lg shadow-md">
|
134
184
|
<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
|
185
|
+
<button class="tab-button active px-4 py-3 text-sm" data-tab="strategies">Estratégias de Reparo (#{alternative_xpaths.size})</button>
|
186
|
+
<button class="tab-button px-4 py-3 text-sm text-gray-600" data-tab="all">Dump Completo (#{all_suggestions.size})</button>
|
137
187
|
</div>
|
138
188
|
<div class="p-6">
|
139
|
-
<div id="
|
140
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">
|
141
|
-
|
189
|
+
<div id="strategies" class="tab-content active">
|
190
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Estratégias de Localização Alternativas</h3>
|
191
|
+
#{repair_strategies_content}
|
142
192
|
</div>
|
143
193
|
<div id="all" class="tab-content">
|
144
|
-
<h3 class="text-lg font-semibold text-
|
145
|
-
<div class="max-h-[
|
194
|
+
<h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
|
195
|
+
<div class="max-h-[800px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
|
146
196
|
</div>
|
147
197
|
</div>
|
148
198
|
</div>
|
@@ -150,21 +200,61 @@ module AppiumFailureHelper
|
|
150
200
|
</div>
|
151
201
|
</div>
|
152
202
|
<script>
|
153
|
-
document.
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
203
|
+
document.addEventListener('DOMContentLoaded', () => {
|
204
|
+
const tabs = document.querySelectorAll('.tab-button');
|
205
|
+
tabs.forEach(tab => {
|
206
|
+
tab.addEventListener('click', (e) => {
|
207
|
+
e.preventDefault();
|
208
|
+
const target = tab.getAttribute('data-tab');
|
209
|
+
tabs.forEach(t => t.classList.remove('active'));
|
210
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
211
|
+
tab.classList.add('active');
|
212
|
+
document.getElementById(target).classList.add('active');
|
213
|
+
});
|
160
214
|
});
|
215
|
+
|
216
|
+
const carousel = document.getElementById('xpath-carousel');
|
217
|
+
if (carousel) {
|
218
|
+
const track = carousel.querySelector('.carousel-track');
|
219
|
+
const items = carousel.querySelectorAll('.carousel-item');
|
220
|
+
const prevButton = carousel.querySelector('.carousel-prev-footer');
|
221
|
+
const nextButton = carousel.querySelector('.carousel-next-footer');
|
222
|
+
const counter = carousel.querySelector('.carousel-counter');
|
223
|
+
const totalItems = items.length;
|
224
|
+
let currentIndex = 0;
|
225
|
+
|
226
|
+
function updateCarousel() {
|
227
|
+
if (totalItems === 0) {
|
228
|
+
if(counter) counter.textContent = "";
|
229
|
+
return;
|
230
|
+
};
|
231
|
+
track.style.transform = `translateX(-${currentIndex * 100}%)`;
|
232
|
+
if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
|
233
|
+
if (prevButton) { prevButton.disabled = currentIndex === 0; }
|
234
|
+
if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
|
235
|
+
}
|
236
|
+
|
237
|
+
if (nextButton) {
|
238
|
+
nextButton.addEventListener('click', () => {
|
239
|
+
if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
|
240
|
+
});
|
241
|
+
}
|
242
|
+
|
243
|
+
if (prevButton) {
|
244
|
+
prevButton.addEventListener('click', () => {
|
245
|
+
if (currentIndex > 0) { currentIndex--; updateCarousel(); }
|
246
|
+
});
|
247
|
+
}
|
248
|
+
|
249
|
+
if (totalItems > 0) { updateCarousel(); }
|
250
|
+
}
|
161
251
|
});
|
162
252
|
</script>
|
163
253
|
</body>
|
164
254
|
</html>
|
165
255
|
HTML_REPORT
|
166
256
|
|
167
|
-
File.write("#{@output_folder}/report_#{timestamp}.html", html_content)
|
257
|
+
File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
|
168
258
|
end
|
169
259
|
end
|
170
260
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module AppiumFailureHelper
|
2
|
+
module SourceCodeAnalyzer
|
3
|
+
PATTERNS = [
|
4
|
+
{ type: 'id', regex: /find_element\((?:id:|:id\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
5
|
+
{ type: 'xpath', regex: /find_element\((?:xpath:|:xpath\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
6
|
+
{ type: 'accessibility_id', regex: /find_element\((?:accessibility_id:|:accessibility_id\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
7
|
+
{ type: 'class_name', regex: /find_element\((?:class_name:|:class_name\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
8
|
+
{ type: 'xpath', regex: /find_element\(:xpath,\s*['"]([^'"]+)['"]\)/ },
|
9
|
+
{ type: 'id', regex: /\s(?:id)\s*\(?['"]([^'"]+)['"]\)?/ },
|
10
|
+
{ type: 'xpath', regex: /\s(?:xpath)\s*\(?['"]([^'"]+)['"]\)?/ },
|
11
|
+
{ type: 'accessibility_id', regex: /\s(?:accessibility_id)\s*\(?['"]([^'"]+)['"]\)?/ }
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
def self.extract_from_exception(exception)
|
15
|
+
location = exception.backtrace.find { |line| line.include?('.rb') && !line.include?('gems') }
|
16
|
+
return {} unless location
|
17
|
+
|
18
|
+
path_match = location.match(/^(.*?):(\d+)(?::in.*)?$/)
|
19
|
+
return {} unless path_match
|
20
|
+
|
21
|
+
file_path = path_match[1]
|
22
|
+
line_number = path_match[2]
|
23
|
+
|
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: 0.
|
4
|
+
version: 1.0.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:
|