appium_failure_helper 1.16.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3b4fc6e89691934ce3c57f04c858ca8eb1671e00f783846ad53c17bc21096ac
|
|
4
|
+
data.tar.gz: ff2c745a44f30be43a60c8b823a1d583431d6686484590377f32007628d3b598
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9831a4ca787ab36d7267b66dd4a31eb6fa2f7aa6c34917f1d08ee14b7b75c2163d1dbff2242e86792e192924e62e873ed9caa8f7cf0e89732ccbcf08922b1588
|
|
7
|
+
data.tar.gz: e5276c7d37ce6df65748da5cee91015a1339e02e1eb03703789eb295bf4d60278b2a2f3cc785888c6b18719a5ff778ba189fe258c1e154acf08537a85472f7dc
|
|
@@ -34,6 +34,10 @@ module AppiumFailureHelper
|
|
|
34
34
|
|
|
35
35
|
page_source = safe_page_source
|
|
36
36
|
failed_info = fetch_failed_element || {}
|
|
37
|
+
if failed_info.empty?
|
|
38
|
+
failed_info = { selector_type: 'unknown', selector_value: 'unknown' }
|
|
39
|
+
report_data[:triage_result] = :unidentified_locator_issue
|
|
40
|
+
end
|
|
37
41
|
|
|
38
42
|
# Extrai todos os elementos da tela
|
|
39
43
|
all_page_elements = []
|
|
@@ -122,33 +126,24 @@ module AppiumFailureHelper
|
|
|
122
126
|
def fetch_failed_element
|
|
123
127
|
msg = @exception&.message.to_s
|
|
124
128
|
|
|
129
|
+
# Formato clássico: using "type" with value "value"
|
|
125
130
|
if (m = msg.match(/using\s+['"](?<type>[^'"]+)['"]\s+with\s+value\s+['"](?<value>.*?)['"]/m))
|
|
126
131
|
return { selector_type: m[:type], selector_value: m[:value] }
|
|
127
132
|
end
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
raw =
|
|
134
|
+
# Novo formato: using "locator"
|
|
135
|
+
if (m = msg.match(/using\s+["'](?<value>[^"']+)["']/))
|
|
136
|
+
raw = m[:value]
|
|
132
137
|
guessed_type = if raw =~ %r{^//|^/}i
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
'xpath'
|
|
139
|
+
elsif raw =~ /^[a-zA-Z0-9\-_:.]+:/
|
|
140
|
+
'id'
|
|
141
|
+
else
|
|
142
|
+
'unknown'
|
|
143
|
+
end
|
|
139
144
|
return { selector_type: guessed_type, selector_value: raw }
|
|
140
145
|
end
|
|
141
146
|
|
|
142
|
-
if (m = msg.match(/"method"\s*:\s*"([^"]+)"[\s,}].*"selector"\s*:\s*"([^"]+)"/i))
|
|
143
|
-
return { selector_type: m[1], selector_value: m[2] }
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
if (m = msg.match(/["']([^"']+)["']/))
|
|
147
|
-
maybe_value = m[1]
|
|
148
|
-
guessed_type = msg[/\b(xpath|id|accessibility id|css)\b/i] ? $&.downcase : 'unknown'
|
|
149
|
-
return { selector_type: guessed_type || 'unknown', selector_value: maybe_value }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
147
|
{}
|
|
153
148
|
end
|
|
154
149
|
|
|
@@ -15,6 +15,15 @@ module AppiumFailureHelper
|
|
|
15
15
|
'XCUIElementTypeCell' => 'cell'
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
|
+
CRITICAL_PATTERNS = [
|
|
19
|
+
/resource-id/i,
|
|
20
|
+
/text/i,
|
|
21
|
+
/content-desc/i,
|
|
22
|
+
/login/i,
|
|
23
|
+
/password/i,
|
|
24
|
+
/email/i
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
18
27
|
def initialize(page_source, platform)
|
|
19
28
|
@doc = Nokogiri::XML(page_source)
|
|
20
29
|
@platform = platform
|
|
@@ -22,55 +31,58 @@ module AppiumFailureHelper
|
|
|
22
31
|
|
|
23
32
|
def analyze
|
|
24
33
|
all_elements_suggestions = []
|
|
25
|
-
|
|
26
|
-
# O XPath '//*' funciona para ambos os XMLs (Android e iOS)
|
|
34
|
+
|
|
27
35
|
@doc.xpath('//*').each do |node|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
next if ['hierarchy', 'AppiumAUT'].include?(node.name)
|
|
37
|
+
|
|
38
|
+
# Extrair todos os atributos do node
|
|
39
|
+
attrs = node.attribute_nodes.to_h { |attr| [attr.name, attr.value] }
|
|
40
|
+
|
|
41
|
+
# Normalização iOS
|
|
42
|
+
if @platform == 'ios'
|
|
43
|
+
attrs['text'] = attrs['label'] || attrs['value']
|
|
44
|
+
attrs['resource-id'] = attrs['name']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attrs['tag'] = node.name
|
|
48
|
+
attrs['critical'] = critical_element?(attrs) # flag de criticidade
|
|
49
|
+
name = suggest_name(node.name, attrs)
|
|
50
|
+
locators = xpath_generator(node.name, attrs)
|
|
51
|
+
|
|
52
|
+
all_elements_suggestions << {
|
|
53
|
+
name: name,
|
|
54
|
+
locators: locators,
|
|
55
|
+
attributes: attrs.merge(path: node.path)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Organiza por criticidade: alto → médio → baixo
|
|
60
|
+
all_elements_suggestions.sort_by do |el|
|
|
61
|
+
el[:attributes][:critical] ? 0 : 1
|
|
44
62
|
end
|
|
45
|
-
all_elements_suggestions.uniq { |s| s[:attributes][:path] }
|
|
46
63
|
end
|
|
47
64
|
|
|
48
65
|
private
|
|
49
66
|
|
|
67
|
+
def critical_element?(attrs)
|
|
68
|
+
CRITICAL_PATTERNS.any? { |regex| attrs.any? { |k,v| v.to_s.match?(regex) } }
|
|
69
|
+
end
|
|
70
|
+
|
|
50
71
|
def suggest_name(tag, attrs)
|
|
51
72
|
type = tag.split('.').last
|
|
52
73
|
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
|
|
55
75
|
priority_attrs = if tag.start_with?('XCUIElementType')
|
|
56
76
|
['name', 'label', 'value']
|
|
57
77
|
else
|
|
58
|
-
['
|
|
78
|
+
['resource-id', 'content-desc', 'text']
|
|
59
79
|
end
|
|
60
80
|
|
|
61
|
-
priority_attrs.
|
|
62
|
-
value = attrs[attr_key]
|
|
63
|
-
if value.is_a?(String) && !value.empty?
|
|
64
|
-
name_base = value
|
|
65
|
-
break
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
81
|
+
name_base = priority_attrs.map { |k| attrs[k] }.compact.find { |v| !v.to_s.empty? }
|
|
69
82
|
name_base ||= type.gsub('XCUIElementType', '')
|
|
70
|
-
|
|
83
|
+
|
|
71
84
|
truncated_name = Utils.truncate(name_base)
|
|
72
85
|
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
|
73
|
-
|
|
74
86
|
"#{pfx}#{sanitized_name}"
|
|
75
87
|
end
|
|
76
88
|
|
|
@@ -84,33 +96,23 @@ module AppiumFailureHelper
|
|
|
84
96
|
|
|
85
97
|
def generate_android_xpaths(tag, attrs)
|
|
86
98
|
locators = []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if attrs['text'] && !attrs['text'].empty?
|
|
91
|
-
locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{Utils.truncate(attrs['text'])}\"]" }
|
|
92
|
-
end
|
|
93
|
-
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
|
94
|
-
locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{Utils.truncate(attrs['content-desc'])}\"]" }
|
|
95
|
-
end
|
|
99
|
+
locators << { strategy: 'id', locator: attrs['resource-id'] } if attrs['resource-id']&.strip&.length.to_i > 0
|
|
100
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{Utils.truncate(attrs['text'])}\"]" } if attrs['text']&.strip&.length.to_i > 0
|
|
101
|
+
locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{Utils.truncate(attrs['content-desc'])}\"]" } if attrs['content-desc']&.strip&.length.to_i > 0
|
|
96
102
|
locators
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
def generate_ios_xpaths(tag, attrs)
|
|
100
106
|
locators = []
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end
|
|
104
|
-
if attrs['label'] && !attrs['label'].empty?
|
|
105
|
-
locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{Utils.truncate(attrs['label'])}\"]" }
|
|
106
|
-
end
|
|
107
|
+
locators << { strategy: 'name', locator: attrs['name'] } if attrs['name']&.strip&.length.to_i > 0
|
|
108
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{Utils.truncate(attrs['label'])}\"]" } if attrs['label']&.strip&.length.to_i > 0
|
|
107
109
|
locators
|
|
108
110
|
end
|
|
109
|
-
|
|
111
|
+
|
|
110
112
|
def generate_unknown_xpaths(tag, attrs)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
attrs.map do |k,v|
|
|
114
|
+
{ strategy: k.to_s, locator: "//#{tag}[@#{k}=\"#{Utils.truncate(v)}\"]" } if v.is_a?(String) && !v.empty?
|
|
115
|
+
end.compact
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
|
-
end
|
|
118
|
+
end
|
|
@@ -3,238 +3,125 @@ module AppiumFailureHelper
|
|
|
3
3
|
def initialize(output_folder, report_data)
|
|
4
4
|
@output_folder = output_folder
|
|
5
5
|
@data = report_data.transform_keys(&:to_sym) rescue report_data
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
# Inicializações seguras para evitar nil e erros de método 'presence'
|
|
8
|
+
@dump = @data[:dump] || []
|
|
9
|
+
@all_page_elements = @data[:all_page_elements].is_a?(Array) ? @data[:all_page_elements] : []
|
|
10
|
+
@alternative_xpaths = if @data[:alternative_xpaths].is_a?(Array) && !@data[:alternative_xpaths].empty?
|
|
11
|
+
@data[:alternative_xpaths]
|
|
12
|
+
else
|
|
13
|
+
[]
|
|
14
|
+
end
|
|
7
15
|
end
|
|
8
16
|
|
|
9
17
|
def generate_all
|
|
10
|
-
generate_xml_report if @page_source
|
|
18
|
+
generate_xml_report if @data[:page_source]
|
|
11
19
|
generate_yaml_reports
|
|
12
20
|
generate_html_report
|
|
13
21
|
end
|
|
14
22
|
|
|
15
23
|
private
|
|
16
|
-
|
|
24
|
+
|
|
17
25
|
def safe_escape_html(value)
|
|
18
26
|
CGI.escapeHTML(value.to_s)
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
def generate_xml_report
|
|
22
30
|
FileUtils.mkdir_p(@output_folder) unless Dir.exist?(@output_folder)
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
page_source = @data[:page_source]
|
|
32
|
+
if page_source && !page_source.empty?
|
|
33
|
+
File.write("#{ @output_folder }/page_source_#{ @data[:timestamp] }.xml", page_source)
|
|
25
34
|
else
|
|
26
35
|
puts "⚠️ Page source está vazio, XML não será gerado"
|
|
27
36
|
end
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
|
|
31
39
|
def generate_yaml_reports
|
|
32
40
|
analysis_report = {
|
|
33
41
|
triage_result: @data[:triage_result],
|
|
34
|
-
exception_class: @data[:exception]
|
|
35
|
-
exception_message: @data[:exception]
|
|
42
|
+
exception_class: @data[:exception]&.class.to_s,
|
|
43
|
+
exception_message: @data[:exception]&.message,
|
|
36
44
|
failed_element: @data[:failed_element],
|
|
37
45
|
best_candidate_analysis: @data[:best_candidate_analysis],
|
|
38
|
-
alternative_xpaths: @
|
|
46
|
+
alternative_xpaths: @alternative_xpaths
|
|
39
47
|
}
|
|
40
|
-
File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
49
|
+
File.write("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", YAML.dump(analysis_report))
|
|
50
|
+
File.write("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", YAML.dump(@all_page_elements)) if @all_page_elements.any?
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
def generate_html_report
|
|
48
|
-
html_content = if @data[:triage_result] == :locator_issue && !(@data[:failed_element] || {}).empty?
|
|
49
|
-
build_full_report
|
|
50
|
-
else
|
|
51
|
-
build_simple_diagnosis_report(
|
|
52
|
-
title: "Diagnóstico Rápido de Falha",
|
|
53
|
-
message: "A análise profunda do seletor não foi executada ou falhou. Verifique a mensagem de erro original e o stack trace."
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
54
|
html_file_path = File.join(@output_folder, "report_#{@data[:timestamp]}.html")
|
|
58
|
-
File.write(html_file_path,
|
|
55
|
+
File.write(html_file_path, build_full_report)
|
|
59
56
|
html_file_path
|
|
60
57
|
end
|
|
61
58
|
|
|
59
|
+
# === HTML ===
|
|
62
60
|
def build_full_report
|
|
63
61
|
failed_info = @data[:failed_element] || {}
|
|
64
|
-
|
|
65
|
-
best_candidate = if @data[:best_candidate_analysis].is_a?(Array)
|
|
66
|
-
@data[:best_candidate_analysis].max_by do |candidate|
|
|
67
|
-
analysis = candidate[:analysis] || {}
|
|
68
|
-
total_score = analysis.values.sum { |v| v[:similarity].to_f rescue 0.0 }
|
|
69
|
-
(total_score / [analysis.size, 1].max)
|
|
70
|
-
end
|
|
71
|
-
else
|
|
72
|
-
{}
|
|
73
|
-
end
|
|
74
|
-
alternative_xpaths = @data[:alternative_xpaths] || []
|
|
62
|
+
best_candidate = select_best_candidate(@data[:best_candidate_analysis])
|
|
75
63
|
timestamp = @data[:timestamp]
|
|
76
64
|
platform = @data[:platform]
|
|
77
65
|
screenshot_base64 = @data[:screenshot_base_64]
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
advanced_analysis_html = build_advanced_analysis(best_candidate)
|
|
68
|
+
repair_strategies_content = build_repair_strategies(@alternative_xpaths)
|
|
69
|
+
all_elements_html = build_all_elements_html(@all_page_elements)
|
|
82
70
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
<<~HTML
|
|
72
|
+
<!DOCTYPE html>
|
|
73
|
+
<html lang="pt-BR">
|
|
74
|
+
<head>
|
|
75
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
+
<title>Relatório de Falha Appium - #{timestamp}</title>
|
|
77
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
78
|
+
<style> .tab-button.active { border-bottom: 2px solid #4f46e5; color: #4f46e5; font-weight: 600; } .tab-content { display: none; } .tab-content.active { display: block; } </style>
|
|
79
|
+
</head>
|
|
80
|
+
<body class="bg-gray-100 p-4 sm:p-8">
|
|
81
|
+
<div class="max-w-7xl mx-auto">
|
|
82
|
+
<header class="mb-8 pb-4 border-b border-gray-300">
|
|
83
|
+
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
|
84
|
+
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.to_s.upcase}</p>
|
|
85
|
+
</header>
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expected = data[:expected].to_s
|
|
98
|
-
actual = data[:actual].to_s
|
|
99
|
-
similarity = data[:similarity].to_f
|
|
100
|
-
match = data[:match]
|
|
101
|
-
|
|
102
|
-
if match == true || similarity == 1.0
|
|
103
|
-
status_color = 'bg-green-500'
|
|
104
|
-
status_icon = '✅'
|
|
105
|
-
status_text = "<b>#{key.capitalize}:</b> Correspondência Exata!"
|
|
106
|
-
elsif similarity > 0.7
|
|
107
|
-
status_color = 'bg-yellow-500'
|
|
108
|
-
status_icon = '⚠️'
|
|
109
|
-
status_text = "<b>#{key.capitalize}:</b> Parecido (Encontrado: '#{CGI.escapeHTML(actual)}')"
|
|
110
|
-
else
|
|
111
|
-
status_color = 'bg-red-500'
|
|
112
|
-
status_icon = '❌'
|
|
113
|
-
status_text = "<b>#{key.capitalize}:</b> Diferente! Esperado: '#{CGI.escapeHTML(expected)}'"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
"<li class='flex items-center text-sm'><span class='w-4 h-4 rounded-full #{status_color} mr-3 flex-shrink-0 flex items-center justify-center text-white text-xs'>#{status_icon}</span><div class='truncate'>#{status_text}</div></li>"
|
|
117
|
-
end.join
|
|
118
|
-
|
|
119
|
-
resource_analysis = best_candidate.dig(:analysis, :"resource-id") || {}
|
|
120
|
-
match = resource_analysis[:match]
|
|
121
|
-
similarity = resource_analysis[:similarity].to_f
|
|
122
|
-
|
|
123
|
-
suggestion_text = if match == true && similarity < 0.7
|
|
124
|
-
"Prefira usar atributos estáveis, como resource-id ou content-desc."\
|
|
125
|
-
"Evite caminhos absolutos (//hierarchy/...); prefira XPaths curtos e relativos."\
|
|
126
|
-
"Use normalize-space() para lidar com espaços e maiúsculas/minúsculas."\
|
|
127
|
-
"Combine atributos, ex: //*[@resource-id='id' and @text='Texto']."\
|
|
128
|
-
"Evite localizar por texto dinâmico, prefira IDs únicos."
|
|
129
|
-
else
|
|
130
|
-
"O `resource-id` pode ter mudado ou o `text` está diferente. Considere usar um seletor mais robusto baseado nos atributos que corresponderam."
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
<<~HTML
|
|
134
|
-
<div class='border border-sky-200 bg-sky-50 p-4 rounded-lg'>
|
|
135
|
-
<h4 class='font-bold text-sky-800 mb-3'>Candidato Mais Provável Encontrado: <span class='font-mono bg-sky-100 text-sky-900 rounded px-2 py-1 text-sm'>#{safe_escape_html(best_candidate[:name])}</span></h4>
|
|
136
|
-
<ul class='space-y-2 mb-4'>#{analysis_details}</ul>
|
|
137
|
-
<div class='bg-sky-100 border-l-4 border-sky-500 text-sky-900 text-sm p-3 rounded-r-lg'>
|
|
138
|
-
<p><b>Sugestão:</b> #{suggestion_text}</p>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
HTML
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
repair_strategies_content = if alternative_xpaths.empty?
|
|
145
|
-
"<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>"
|
|
146
|
-
else
|
|
147
|
-
pages = alternative_xpaths.each_slice(6).to_a
|
|
148
|
-
carousel_items = pages.map do |page_strategies|
|
|
149
|
-
strategy_list_html = page_strategies.map do |strategy|
|
|
150
|
-
reliability_color = case strategy[:reliability]
|
|
151
|
-
when :alta then 'bg-green-100 text-green-800'
|
|
152
|
-
when :media then 'bg-yellow-100 text-yellow-800'
|
|
153
|
-
else 'bg-red-100 text-red-800'
|
|
154
|
-
end
|
|
155
|
-
# CORREÇÃO: Adiciona o tipo de estratégia (ID, XPATH) ao lado do seletor
|
|
156
|
-
<<~STRATEGY_ITEM
|
|
157
|
-
<li class='border-b border-gray-200 py-3 last:border-b-0'>
|
|
158
|
-
<div class='flex justify-between items-center mb-1'>
|
|
159
|
-
<p class='font-semibold text-indigo-800 text-sm'>#{safe_escape_html(strategy[:name])}</p>
|
|
160
|
-
<span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{safe_escape_html(strategy[:reliability].to_s.capitalize)}</span>
|
|
161
|
-
</div>
|
|
162
|
-
<div class='bg-gray-800 text-white p-2 rounded mt-1 text-xs whitespace-pre-wrap break-words font-mono'>
|
|
163
|
-
<span class='font-bold text-indigo-400'>#{safe_escape_html(strategy[:strategy].to_s.upcase)}:</span>
|
|
164
|
-
<code class='ml-1'>#{safe_escape_html(strategy[:locator])}</code>
|
|
165
|
-
</div>
|
|
166
|
-
</li>
|
|
167
|
-
STRATEGY_ITEM
|
|
168
|
-
end.join
|
|
169
|
-
"<div class='carousel-item w-full flex-shrink-0'><ul>#{strategy_list_html}</ul></div>"
|
|
170
|
-
end.join
|
|
171
|
-
|
|
172
|
-
<<~CAROUSEL
|
|
173
|
-
<div id="xpath-carousel" class="relative">
|
|
174
|
-
<div class="overflow-hidden">
|
|
175
|
-
<div class="carousel-track flex transition-transform duration-300 ease-in-out">
|
|
176
|
-
#{carousel_items}
|
|
87
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
88
|
+
<div class="lg:col-span-1 space-y-6">
|
|
89
|
+
<div class="bg-white p-4 rounded-lg shadow-md border border-red-200">
|
|
90
|
+
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
|
91
|
+
<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'>#{safe_escape_html(failed_info[:selector_type])}</span></p>
|
|
92
|
+
<p class="text-sm text-gray-700 font-medium">Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-words'>#{safe_escape_html(failed_info[:selector_value])}</span></p>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="bg-white p-4 rounded-lg shadow-md">
|
|
95
|
+
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
|
96
|
+
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
|
177
97
|
</div>
|
|
178
98
|
</div>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
Próximo >
|
|
186
|
-
</button>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
CAROUSEL
|
|
190
|
-
end
|
|
191
|
-
<<~HTML_REPORT
|
|
192
|
-
<!DOCTYPE html>
|
|
193
|
-
<html lang="pt-BR">
|
|
194
|
-
<head>
|
|
195
|
-
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
196
|
-
<title>Relatório de Falha Appium - #{timestamp}</title>
|
|
197
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
198
|
-
<style> .tab-button.active { border-bottom: 2px solid #4f46e5; color: #4f46e5; font-weight: 600; } .tab-content { display: none; } .tab-content.active { display: block; } </style>
|
|
199
|
-
</head>
|
|
200
|
-
<body class="bg-gray-100 p-4 sm:p-8">
|
|
201
|
-
<div class="max-w-7xl mx-auto">
|
|
202
|
-
<header class="mb-8 pb-4 border-b border-gray-300">
|
|
203
|
-
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
|
204
|
-
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.to_s.upcase}</p>
|
|
205
|
-
</header>
|
|
206
|
-
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
207
|
-
<div class="lg:col-span-1 space-y-6">
|
|
208
|
-
<div class="bg-white p-4 rounded-lg shadow-md border border-red-200">
|
|
209
|
-
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
|
210
|
-
#{failed_info_content}
|
|
211
|
-
</div>
|
|
212
|
-
<div class="bg-white p-4 rounded-lg shadow-md">
|
|
213
|
-
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
|
214
|
-
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
|
99
|
+
|
|
100
|
+
<div class="lg:col-span-2">
|
|
101
|
+
<div class="bg-white rounded-lg shadow-md">
|
|
102
|
+
<div class="flex border-b border-gray-200">
|
|
103
|
+
<button class="tab-button active px-4 py-3 text-sm" data-tab="analysis">Análise Avançada</button>
|
|
104
|
+
<button class="tab-button px-4 py-3 text-sm text-gray-600" data-tab="all">Dump Completo (#{@all_page_elements.size})</button>
|
|
215
105
|
</div>
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
106
|
+
|
|
107
|
+
<div class="p-6">
|
|
108
|
+
<div id="analysis" class="tab-content active">
|
|
109
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Diagnóstico por Atributos Ponderados</h3>
|
|
110
|
+
#{advanced_analysis_html}
|
|
111
|
+
#{repair_strategies_content}
|
|
222
112
|
</div>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
#{
|
|
228
|
-
</div>
|
|
229
|
-
<div id="all" class="tab-content">
|
|
230
|
-
<h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
|
|
231
|
-
<div class="max-h-[800px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
|
|
113
|
+
|
|
114
|
+
<div id="all" class="tab-content">
|
|
115
|
+
<h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
|
|
116
|
+
<div class="max-h-[800px] overflow-y-auto space-y-2">
|
|
117
|
+
#{all_elements_html}
|
|
232
118
|
</div>
|
|
233
119
|
</div>
|
|
234
120
|
</div>
|
|
235
121
|
</div>
|
|
236
122
|
</div>
|
|
237
123
|
</div>
|
|
124
|
+
|
|
238
125
|
<script>
|
|
239
126
|
document.addEventListener('DOMContentLoaded', () => {
|
|
240
127
|
const tabs = document.querySelectorAll('.tab-button');
|
|
@@ -248,111 +135,166 @@ module AppiumFailureHelper
|
|
|
248
135
|
document.getElementById(target).classList.add('active');
|
|
249
136
|
});
|
|
250
137
|
});
|
|
138
|
+
|
|
139
|
+
const carousel = document.getElementById('xpath-carousel');
|
|
140
|
+
if (carousel) {
|
|
141
|
+
const track = carousel.querySelector('.carousel-track');
|
|
142
|
+
const items = carousel.querySelectorAll('.carousel-item');
|
|
143
|
+
const prevButton = carousel.querySelector('.carousel-prev-footer');
|
|
144
|
+
const nextButton = carousel.querySelector('.carousel-next-footer');
|
|
145
|
+
const counter = carousel.querySelector('.carousel-counter');
|
|
146
|
+
const totalItems = items.length;
|
|
147
|
+
let currentIndex = 0;
|
|
148
|
+
|
|
149
|
+
function updateCarousel() {
|
|
150
|
+
if(totalItems === 0) { if(counter) counter.textContent = "Nenhuma estratégia"; return; }
|
|
151
|
+
track.style.transform = `translateX(-${currentIndex * 100}%)`;
|
|
152
|
+
if(counter) counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`;
|
|
153
|
+
if(prevButton) prevButton.disabled = currentIndex === 0;
|
|
154
|
+
if(nextButton) nextButton.disabled = currentIndex === totalItems - 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if(nextButton) nextButton.addEventListener('click', () => { if(currentIndex < totalItems - 1) currentIndex++; updateCarousel(); });
|
|
158
|
+
if(prevButton) prevButton.addEventListener('click', () => { if(currentIndex > 0) currentIndex--; updateCarousel(); });
|
|
159
|
+
if(totalItems > 0) updateCarousel();
|
|
160
|
+
}
|
|
251
161
|
});
|
|
252
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
253
|
-
const tabs = document.querySelectorAll('.tab-button');
|
|
254
|
-
tabs.forEach(tab => {
|
|
255
|
-
tab.addEventListener('click', (e) => {
|
|
256
|
-
e.preventDefault();
|
|
257
|
-
const target = tab.getAttribute('data-tab');
|
|
258
|
-
tabs.forEach(t => t.classList.remove('active'));
|
|
259
|
-
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
260
|
-
tab.classList.add('active');
|
|
261
|
-
document.getElementById(target).classList.add('active');
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
const carousel = document.getElementById('xpath-carousel');
|
|
266
|
-
if (carousel) {
|
|
267
|
-
const track = carousel.querySelector('.carousel-track');
|
|
268
|
-
const items = carousel.querySelectorAll('.carousel-item');
|
|
269
|
-
const prevButton = carousel.querySelector('.carousel-prev-footer');
|
|
270
|
-
const nextButton = carousel.querySelector('.carousel-next-footer');
|
|
271
|
-
const counter = carousel.querySelector('.carousel-counter');
|
|
272
|
-
const totalItems = items.length;
|
|
273
|
-
let currentIndex = 0;
|
|
274
|
-
|
|
275
|
-
function updateCarousel() {
|
|
276
|
-
if (totalItems === 0) {
|
|
277
|
-
if(counter) counter.textContent = "";
|
|
278
|
-
return;
|
|
279
|
-
};
|
|
280
|
-
track.style.transform = `translateX(-${currentIndex * 100}%)`;
|
|
281
|
-
if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
|
|
282
|
-
if (prevButton) { prevButton.disabled = currentIndex === 0; }
|
|
283
|
-
if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (nextButton) {
|
|
287
|
-
nextButton.addEventListener('click', () => {
|
|
288
|
-
if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (prevButton) {
|
|
293
|
-
prevButton.addEventListener('click', () => {
|
|
294
|
-
if (currentIndex > 0) { currentIndex--; updateCarousel(); }
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (totalItems > 0) { updateCarousel(); }
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
162
|
</script>
|
|
302
|
-
</
|
|
303
|
-
|
|
304
|
-
|
|
163
|
+
</div>
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
HTML
|
|
305
167
|
end
|
|
306
168
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
169
|
+
# === Helpers ===
|
|
170
|
+
def select_best_candidate(candidates)
|
|
171
|
+
return {} unless candidates.is_a?(Array) && candidates.any?
|
|
172
|
+
candidates.max_by do |candidate|
|
|
173
|
+
analysis = candidate[:analysis] || {}
|
|
174
|
+
total_score = analysis.values.sum { |v| v[:similarity].to_f rescue 0.0 }
|
|
175
|
+
total_score / [analysis.size, 1].max
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_advanced_analysis(best_candidate)
|
|
180
|
+
return "<p class='text-gray-500'>Nenhum candidato provável encontrado.</p>" if best_candidate.nil? || best_candidate.empty?
|
|
181
|
+
|
|
182
|
+
(best_candidate[:analysis] || {}).map do |key, data|
|
|
183
|
+
data ||= {}
|
|
184
|
+
match = data[:match]
|
|
185
|
+
similarity = data[:similarity].to_f
|
|
186
|
+
expected = data[:expected].to_s
|
|
187
|
+
actual = data[:actual].to_s
|
|
188
|
+
|
|
189
|
+
status_color, status_icon, status_text, bg_color = if match || similarity == 1.0
|
|
190
|
+
['text-green-700', '✅', "Correspondência Exata!", 'bg-green-50']
|
|
191
|
+
elsif similarity > 0.7
|
|
192
|
+
['text-yellow-800', '⚠️', "Parecido (Encontrado: '#{CGI.escapeHTML(actual)}')", 'bg-yellow-50']
|
|
193
|
+
else
|
|
194
|
+
['text-red-700', '❌', "Diferente! Esperado: '#{CGI.escapeHTML(expected)}'", 'bg-red-50']
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
<<~HTML
|
|
198
|
+
<div class="p-4 rounded-lg mb-4 #{bg_color} border border-gray-200 shadow-sm">
|
|
199
|
+
<div class="flex items-center mb-2">
|
|
200
|
+
<span class="text-xl mr-2">#{status_icon}</span>
|
|
201
|
+
<h4 class="font-semibold text-gray-900 text-sm">#{key.capitalize}</h4>
|
|
202
|
+
</div>
|
|
203
|
+
<p class="text-sm #{status_color} ml-6 break-words">
|
|
204
|
+
#{status_text}<br>
|
|
205
|
+
<span class="font-mono text-xs text-gray-700">Resource-id: #{CGI.escapeHTML(data[:actual].to_s)}</span>
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
HTML
|
|
209
|
+
end.join
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_repair_strategies(strategies)
|
|
214
|
+
return "<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>" if strategies.empty?
|
|
215
|
+
|
|
216
|
+
# Ordena por confiabilidade: alta > media > baixa
|
|
217
|
+
order = { alta: 3, media: 2, baixa: 1 }
|
|
218
|
+
strategies = strategies.sort_by { |s| -order[s[:reliability]] }
|
|
219
|
+
|
|
220
|
+
items_per_page = 4
|
|
221
|
+
pages = strategies.each_slice(items_per_page).to_a
|
|
222
|
+
|
|
223
|
+
pages_html = pages.map do |page|
|
|
224
|
+
page_items = page.map do |s|
|
|
225
|
+
reliability_color = case s[:reliability]
|
|
226
|
+
when :alta then 'bg-green-100 text-green-800'
|
|
227
|
+
when :media then 'bg-yellow-100 text-yellow-800'
|
|
228
|
+
else 'bg-red-100 text-red-800'
|
|
229
|
+
end
|
|
230
|
+
<<~STR
|
|
231
|
+
<li class='border-b border-gray-200 py-3 last:border-b-0'>
|
|
232
|
+
<div class='flex justify-between items-center mb-1'>
|
|
233
|
+
<p class='font-semibold text-indigo-800 text-sm'>#{safe_escape_html(s[:name])}</p>
|
|
234
|
+
<span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{safe_escape_html(s[:reliability].to_s.capitalize)}</span>
|
|
235
|
+
</div>
|
|
236
|
+
<div class='bg-gray-800 text-white p-2 rounded mt-1 text-xs whitespace-pre-wrap break-words font-mono'>
|
|
237
|
+
<span class='font-bold text-indigo-400'>#{safe_escape_html(s[:strategy].to_s.upcase)}:</span>
|
|
238
|
+
<code class='ml-1'>#{safe_escape_html(s[:locator])}</code>
|
|
351
239
|
</div>
|
|
240
|
+
</li>
|
|
241
|
+
STR
|
|
242
|
+
end.join
|
|
243
|
+
"<div class='carousel-item w-full flex-shrink-0'><ul>#{page_items}</ul></div>"
|
|
244
|
+
end.join
|
|
245
|
+
|
|
246
|
+
<<~HTML
|
|
247
|
+
<div id="xpath-carousel" class="relative">
|
|
248
|
+
<div class="overflow-hidden">
|
|
249
|
+
<div class="carousel-track flex transition-transform duration-300 ease-in-out">
|
|
250
|
+
#{pages_html}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="flex items-center justify-center space-x-4 mt-4">
|
|
254
|
+
<button class="carousel-prev-footer bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors">< Anterior</button>
|
|
255
|
+
<div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
|
|
256
|
+
<button class="carousel-next-footer bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors">Próximo ></button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
HTML
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_all_elements_html(elements)
|
|
265
|
+
return "<p class='text-gray-500'>Nenhum elemento capturado.</p>" if elements.empty?
|
|
266
|
+
|
|
267
|
+
elements.map do |el|
|
|
268
|
+
attrs = el[:attributes] || {}
|
|
269
|
+
locators = el[:locators] || []
|
|
270
|
+
critical = attrs[:critical] || false
|
|
271
|
+
bg_color = critical ? 'bg-yellow-50 border-yellow-300' : 'bg-white border-gray-200'
|
|
272
|
+
|
|
273
|
+
# Lista de atributos
|
|
274
|
+
attributes_html = attrs.map do |k, v|
|
|
275
|
+
"<li class='flex justify-between items-start p-1 text-xs font-mono'><span class='font-semibold text-gray-700'>#{CGI.escapeHTML(k.to_s)}</span>: <span class='text-gray-800 ml-2 break-words'>#{CGI.escapeHTML(v.to_s)}</span></li>"
|
|
276
|
+
end.join
|
|
277
|
+
|
|
278
|
+
# Lista de estratégias de XPath / locators
|
|
279
|
+
locators_html = locators.map do |loc|
|
|
280
|
+
"<li class='flex justify-between items-start p-1 text-xs font-mono bg-gray-50 rounded-md mb-1'><span class='font-semibold text-indigo-700'>#{CGI.escapeHTML(loc[:strategy].to_s.upcase)}</span>: <span class='text-gray-800 ml-2 break-words'>#{CGI.escapeHTML(loc[:locator].to_s)}</span></li>"
|
|
281
|
+
end.join
|
|
282
|
+
|
|
283
|
+
<<~HTML
|
|
284
|
+
<details class="mb-2 border-l-4 #{bg_color} rounded-md p-2">
|
|
285
|
+
<summary class="font-semibold text-sm text-gray-800 cursor-pointer">#{CGI.escapeHTML(el[:name].to_s)}</summary>
|
|
286
|
+
<ul class="mt-1 space-y-1">
|
|
287
|
+
#{attributes_html}
|
|
288
|
+
</ul>
|
|
289
|
+
<div class="mt-2">
|
|
290
|
+
<p class="font-semibold text-gray-600 text-xs mb-1">Estratégias de Localização:</p>
|
|
291
|
+
<ul class="space-y-1">
|
|
292
|
+
#{locators_html}
|
|
293
|
+
</ul>
|
|
352
294
|
</div>
|
|
353
|
-
</
|
|
354
|
-
|
|
355
|
-
|
|
295
|
+
</details>
|
|
296
|
+
HTML
|
|
297
|
+
end.join
|
|
356
298
|
end
|
|
357
299
|
end
|
|
358
|
-
end
|
|
300
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: appium_failure_helper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.16.
|
|
4
|
+
version: 1.16.1
|
|
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-11-
|
|
11
|
+
date: 2025-11-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|