appium_failure_helper 0.6.0 → 0.6.2
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/capture.rb +221 -296
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87d48b6025e058dfc945cc71675a8389ffd7c219a8bedccf8994327b10d02760
|
4
|
+
data.tar.gz: 700ae3934bd4413d7ea30ac589493dca717e199dd0a69374156ec2f4bda966b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83321c33adbc356d531abe28f2f792b635e5e2658676c7510afabd263e4c7f99b4d4c5dcbaaf0d20bba569a62841b812f50015b5901f3e860ebe0b6f44bbbcc2
|
7
|
+
data.tar.gz: 35935a71ebd0ff920cfeee986d0918337f706146b680c7a5f85bba334dcc790d0ca4c01bb8bcf1b3aec2efaa95a5d758fc6b416ece69bd2504b3c9b59ac45a91
|
@@ -3,188 +3,165 @@ require 'fileutils'
|
|
3
3
|
require 'base64'
|
4
4
|
require 'yaml'
|
5
5
|
require 'logger'
|
6
|
+
require 'did_you_mean'
|
7
|
+
require 'cgi' # Adicionado para garantir o escape de HTML
|
6
8
|
|
7
9
|
module AppiumFailureHelper
|
8
10
|
class Capture
|
11
|
+
# ... (Constantes PREFIX, MAX_VALUE_LENGTH, @@logger permanecem iguais) ...
|
9
12
|
PREFIX = {
|
10
|
-
'android.widget.Button' => 'btn',
|
11
|
-
'android.widget.
|
12
|
-
'android.widget.
|
13
|
-
'android.widget.
|
14
|
-
'android.widget.
|
15
|
-
'android.widget.
|
16
|
-
'android.widget.
|
17
|
-
'android.widget.
|
18
|
-
'
|
19
|
-
'
|
20
|
-
'
|
21
|
-
'
|
22
|
-
'android.widget.ScrollView' => 'scroll',
|
23
|
-
'android.webkit.WebView' => 'web',
|
24
|
-
'android.widget.Spinner' => 'spin',
|
25
|
-
'XCUIElementTypeButton' => 'btn',
|
26
|
-
'XCUIElementTypeStaticText' => 'txt',
|
27
|
-
'XCUIElementTypeTextField' => 'input',
|
28
|
-
'XCUIElementTypeImage' => 'img',
|
29
|
-
'XCUIElementTypeSwitch' => 'switch',
|
30
|
-
'XCUIElementTypeScrollView' => 'scroll',
|
31
|
-
'XCUIElementTypeOther' => 'elm',
|
32
|
-
'XCUIElementTypeCell' => 'cell',
|
13
|
+
'android.widget.Button' => 'btn', 'android.widget.TextView' => 'txt',
|
14
|
+
'android.widget.ImageView' => 'img', 'android.widget.EditText' => 'input',
|
15
|
+
'android.widget.CheckBox' => 'chk', 'android.widget.RadioButton' => 'radio',
|
16
|
+
'android.widget.Switch' => 'switch', 'android.widget.ViewGroup' => 'group',
|
17
|
+
'android.widget.View' => 'view', 'android.widget.FrameLayout' => 'frame',
|
18
|
+
'android.widget.LinearLayout' => 'linear', 'android.widget.RelativeLayout' => 'relative',
|
19
|
+
'android.widget.ScrollView' => 'scroll', 'android.webkit.WebView' => 'web',
|
20
|
+
'android.widget.Spinner' => 'spin', 'XCUIElementTypeButton' => 'btn',
|
21
|
+
'XCUIElementTypeStaticText' => 'txt', 'XCUIElementTypeTextField' => 'input',
|
22
|
+
'XCUIElementTypeImage' => 'img', 'XCUIElementTypeSwitch' => 'switch',
|
23
|
+
'XCUIElementTypeScrollView' => 'scroll', 'XCUIElementTypeOther' => 'elm',
|
24
|
+
'XCUIElementTypeCell' => 'cell'
|
33
25
|
}.freeze
|
34
|
-
|
35
26
|
MAX_VALUE_LENGTH = 100
|
36
27
|
@@logger = nil
|
37
28
|
|
29
|
+
# --- MÉTODO PRINCIPAL (SEM ALTERAÇÕES) ---
|
38
30
|
def self.handler_failure(driver, exception)
|
39
31
|
begin
|
40
32
|
self.setup_logger unless @@logger
|
41
|
-
|
42
|
-
# Remove a pasta reports_failure ao iniciar uma nova execução
|
43
|
-
FileUtils.rm_rf("reports_failure")
|
44
|
-
@@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
|
45
|
-
|
46
33
|
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
47
34
|
output_folder = "reports_failure/failure_#{timestamp}"
|
48
|
-
|
49
35
|
FileUtils.mkdir_p(output_folder)
|
50
36
|
@@logger.info("Pasta de saída criada: #{output_folder}")
|
51
|
-
|
52
|
-
# Captura o Base64 e salva o PNG
|
53
37
|
screenshot_base64 = driver.screenshot_as(:base64)
|
54
|
-
screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
|
55
|
-
File.open(screenshot_path, 'wb') do |f|
|
56
|
-
f.write(Base64.decode64(screenshot_base64))
|
57
|
-
end
|
58
|
-
@@logger.info("Screenshot salvo em #{screenshot_path}")
|
59
|
-
|
60
38
|
page_source = driver.page_source
|
61
|
-
|
62
|
-
File.write(xml_path, page_source)
|
63
|
-
@@logger.info("Page source salvo em #{xml_path}")
|
64
|
-
|
39
|
+
File.write("#{output_folder}/page_source_#{timestamp}.xml", page_source)
|
65
40
|
doc = Nokogiri::XML(page_source)
|
66
41
|
platform = driver.capabilities['platformName']&.downcase || 'unknown'
|
67
|
-
|
68
42
|
failed_element_info = self.extract_info_from_exception(exception)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
unique_key = "#{node.name}|#{attrs['resource-id'].to_s}|#{attrs['content-desc'].to_s}|#{attrs['text'].to_s}"
|
78
|
-
|
79
|
-
unless seen_elements[unique_key]
|
80
|
-
name = self.suggest_name(node.name, attrs)
|
81
|
-
locators = self.xpath_generator(node.name, attrs, platform)
|
82
|
-
|
83
|
-
all_elements_suggestions << { name: name, locators: locators }
|
84
|
-
seen_elements[unique_key] = true
|
85
|
-
end
|
43
|
+
local_element_map = self.load_local_element_map
|
44
|
+
de_para_result = nil
|
45
|
+
logical_name_key = failed_element_info[:selector_value].to_s.gsub(/^#/, '')
|
46
|
+
if local_element_map.key?(logical_name_key)
|
47
|
+
de_para_result = {
|
48
|
+
logical_name: logical_name_key,
|
49
|
+
correct_locator: local_element_map[logical_name_key]
|
50
|
+
}
|
86
51
|
end
|
87
|
-
|
88
|
-
|
52
|
+
all_elements_suggestions = self.get_all_elements_from_screen(doc, platform)
|
53
|
+
similar_elements = self.find_similar_elements(failed_element_info, all_elements_suggestions)
|
89
54
|
targeted_report = {
|
90
55
|
failed_element: failed_element_info,
|
91
|
-
similar_elements:
|
56
|
+
similar_elements: similar_elements,
|
57
|
+
de_para_analysis: de_para_result
|
92
58
|
}
|
93
|
-
|
94
|
-
|
95
|
-
targeted_report[:similar_elements] = self.find_similar_elements(doc, failed_element_info, platform)
|
96
|
-
end
|
97
|
-
|
98
|
-
targeted_yaml_path = "#{output_folder}/failure_analysis_#{timestamp}.yaml"
|
99
|
-
File.open(targeted_yaml_path, 'w') do |f|
|
100
|
-
f.write(YAML.dump(targeted_report))
|
101
|
-
end
|
102
|
-
@@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
|
103
|
-
|
104
|
-
# --- Geração do Relatório COMPLETO (2) ---
|
105
|
-
full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
|
106
|
-
File.open(full_dump_yaml_path, 'w') do |f|
|
107
|
-
f.write(YAML.dump(all_elements_suggestions))
|
108
|
-
end
|
109
|
-
@@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
|
110
|
-
|
111
|
-
# --- Geração do Relatório HTML (3) ---
|
112
|
-
html_report_path = "#{output_folder}/report_#{timestamp}.html"
|
59
|
+
File.open("#{output_folder}/failure_analysis_#{timestamp}.yaml", 'w') { |f| f.write(YAML.dump(targeted_report)) }
|
60
|
+
File.open("#{output_folder}/all_elements_dump_#{timestamp}.yaml", 'w') { |f| f.write(YAML.dump(all_elements_suggestions)) }
|
113
61
|
html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
|
114
|
-
File.write(
|
115
|
-
@@logger.info("
|
116
|
-
|
62
|
+
File.write("#{output_folder}/report_#{timestamp}.html", html_content)
|
63
|
+
@@logger.info("Relatórios gerados com sucesso em: #{output_folder}")
|
117
64
|
rescue => e
|
118
65
|
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
119
66
|
end
|
120
67
|
end
|
121
68
|
|
122
69
|
private
|
123
|
-
|
124
|
-
def self.
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
70
|
+
|
71
|
+
def self.load_local_element_map
|
72
|
+
elements_map = {}
|
73
|
+
glob_path = File.join(Dir.pwd, 'features', 'elements', '**', '*.yaml')
|
74
|
+
Dir.glob(glob_path).each do |file|
|
75
|
+
begin
|
76
|
+
data = YAML.load_file(file)
|
77
|
+
if data.is_a?(Hash)
|
78
|
+
data.each do |key, value|
|
79
|
+
if value.is_a?(Hash) && value['tipoBusca'] && value['valor']
|
80
|
+
elements_map[key] = value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue => e
|
85
|
+
@@logger.warn("Aviso: Erro ao carregar o arquivo de elementos #{file}: #{e.message}") if @@logger
|
86
|
+
end
|
129
87
|
end
|
88
|
+
elements_map
|
130
89
|
end
|
131
|
-
|
90
|
+
|
91
|
+
def self.setup_logger
|
92
|
+
@@logger = Logger.new(STDOUT)
|
93
|
+
@@logger.level = Logger::INFO
|
94
|
+
@@logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n" }
|
95
|
+
end
|
96
|
+
|
132
97
|
def self.extract_info_from_exception(exception)
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
selector_type = match[1]&.strip || 'Unknown'
|
149
|
-
|
150
|
-
info[:selector_type] = selector_type
|
151
|
-
info[:selector_value] = selector_value.gsub(/['"]/, '')
|
152
|
-
return info
|
98
|
+
message = exception.message
|
99
|
+
info = {}
|
100
|
+
patterns = [
|
101
|
+
/element with locator ['"]?(#?\w+)['"]?/i,
|
102
|
+
/(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
|
103
|
+
/no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
|
104
|
+
/(?:with the resource-id|with the accessibility-id) ['"]?(.+?)['"]?/i
|
105
|
+
]
|
106
|
+
patterns.each do |pattern|
|
107
|
+
match = message.match(pattern)
|
108
|
+
if match
|
109
|
+
info[:selector_value] = match.captures.last.strip.gsub(/['"]/, '')
|
110
|
+
info[:selector_type] = match.captures.size > 1 ? match.captures[0].strip.gsub(/['"]/, '') : 'id' # Padroniza para 'id'
|
111
|
+
return info
|
112
|
+
end
|
153
113
|
end
|
154
|
-
|
155
|
-
info
|
114
|
+
info
|
156
115
|
end
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
116
|
+
|
117
|
+
|
118
|
+
def self.find_similar_elements(failed_element_info, all_page_suggestions)
|
119
|
+
failed_locator_value = failed_element_info[:selector_value]
|
120
|
+
failed_locator_type = failed_element_info[:selector_type]
|
121
|
+
return [] unless failed_locator_value && failed_locator_type
|
122
|
+
|
123
|
+
# Padroniza os tipos de localizador (ex: 'resource-id' vira 'id')
|
124
|
+
normalized_failed_type = failed_locator_type.downcase.include?('id') ? 'id' : failed_locator_type
|
125
|
+
|
126
|
+
cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
127
|
+
similarities = []
|
128
|
+
|
129
|
+
all_page_suggestions.each do |suggestion|
|
130
|
+
# Procura por um localizador na sugestão que tenha a MESMA ESTRATÉGIA do localizador que falhou
|
131
|
+
candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
|
132
|
+
next unless candidate_locator
|
163
133
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
(attrs['accessibility-id']&.downcase&.include?(selector_value) ||
|
173
|
-
attrs['label']&.downcase&.include?(selector_value) ||
|
174
|
-
attrs['name']&.downcase&.include?(selector_value))
|
175
|
-
else
|
176
|
-
false
|
177
|
-
end
|
178
|
-
|
179
|
-
if is_similar
|
180
|
-
name = self.suggest_name(node.name, attrs)
|
181
|
-
locators = self.xpath_generator(node.name, attrs, platform)
|
182
|
-
similar_elements << { name: name, locators: locators }
|
134
|
+
cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
135
|
+
distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
|
136
|
+
max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
|
137
|
+
next if max_len.zero?
|
138
|
+
|
139
|
+
similarity_score = 1.0 - (distance.to_f / max_len)
|
140
|
+
if similarity_score > 0.8 # Aumenta o limiar para maior precisão
|
141
|
+
similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score }
|
183
142
|
end
|
184
143
|
end
|
185
|
-
|
144
|
+
similarities.sort_by { |s| -s[:score] }.first(5)
|
186
145
|
end
|
187
146
|
|
147
|
+
# ... (get_all_elements_from_screen, truncate, suggest_name permanecem iguais) ...
|
148
|
+
def self.get_all_elements_from_screen(doc, platform)
|
149
|
+
seen_elements = {}
|
150
|
+
all_elements_suggestions = []
|
151
|
+
doc.xpath('//*').each do |node|
|
152
|
+
next if ['hierarchy', 'AppiumAUT'].include?(node.name)
|
153
|
+
attrs = node.attributes.transform_values(&:value)
|
154
|
+
unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
|
155
|
+
unless seen_elements[unique_key]
|
156
|
+
name = self.suggest_name(node.name, attrs)
|
157
|
+
locators = self.xpath_generator(node.name, attrs, platform)
|
158
|
+
all_elements_suggestions << { name: name, locators: locators }
|
159
|
+
seen_elements[unique_key] = true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
all_elements_suggestions
|
163
|
+
end
|
164
|
+
|
188
165
|
def self.truncate(value)
|
189
166
|
return value unless value.is_a?(String)
|
190
167
|
value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
|
@@ -195,7 +172,13 @@ module AppiumFailureHelper
|
|
195
172
|
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
196
173
|
name_base = nil
|
197
174
|
|
198
|
-
|
175
|
+
priority_attrs = if tag.start_with?('XCUIElementType')
|
176
|
+
['name', 'label', 'value']
|
177
|
+
else
|
178
|
+
['content-desc', 'text', 'resource-id']
|
179
|
+
end
|
180
|
+
|
181
|
+
priority_attrs.each do |attr_key|
|
199
182
|
value = attrs[attr_key]
|
200
183
|
if value.is_a?(String) && !value.empty?
|
201
184
|
name_base = value
|
@@ -203,7 +186,8 @@ module AppiumFailureHelper
|
|
203
186
|
end
|
204
187
|
end
|
205
188
|
|
206
|
-
|
189
|
+
# Se nenhum atributo de prioridade for encontrado, usa o nome da classe.
|
190
|
+
name_base ||= type.gsub('XCUIElementType', '') # Remove o prefixo longo do iOS
|
207
191
|
|
208
192
|
truncated_name = truncate(name_base)
|
209
193
|
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
@@ -213,209 +197,150 @@ module AppiumFailureHelper
|
|
213
197
|
|
214
198
|
def self.xpath_generator(tag, attrs, platform)
|
215
199
|
case platform
|
216
|
-
when 'android'
|
217
|
-
|
218
|
-
|
219
|
-
self.generate_ios_xpaths(tag, attrs)
|
220
|
-
else
|
221
|
-
self.generate_unknown_xpaths(tag, attrs)
|
200
|
+
when 'android' then self.generate_android_xpaths(tag, attrs)
|
201
|
+
when 'ios' then self.generate_ios_xpaths(tag, attrs)
|
202
|
+
else self.generate_unknown_xpaths(tag, attrs)
|
222
203
|
end
|
223
204
|
end
|
224
205
|
|
225
206
|
def self.generate_android_xpaths(tag, attrs)
|
226
207
|
locators = []
|
227
|
-
|
228
|
-
if attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['text'] && !attrs['text'].empty?
|
229
|
-
locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @text=\"#{self.truncate(attrs['text'])}\"]" }
|
230
|
-
elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
|
231
|
-
locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
232
|
-
end
|
233
|
-
|
234
208
|
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
235
|
-
|
209
|
+
# CORREÇÃO: A estratégia para resource-id é 'id', e o valor é o próprio ID.
|
210
|
+
locators << { strategy: 'id', locator: attrs['resource-id'] }
|
236
211
|
end
|
237
|
-
|
238
|
-
if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
|
239
|
-
id_part = attrs['resource-id'].split(':id/').last
|
240
|
-
locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, \"#{id_part}\")]" }
|
241
|
-
end
|
242
|
-
|
243
212
|
if attrs['text'] && !attrs['text'].empty?
|
244
|
-
locators << { strategy: '
|
213
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{truncate(attrs['text'])}\"]" }
|
245
214
|
end
|
246
215
|
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
247
|
-
locators << { strategy: '
|
216
|
+
locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{truncate(attrs['content-desc'])}\"]" }
|
248
217
|
end
|
249
|
-
|
250
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
251
|
-
|
252
218
|
locators
|
253
219
|
end
|
254
220
|
|
255
221
|
def self.generate_ios_xpaths(tag, attrs)
|
256
222
|
locators = []
|
257
|
-
|
258
|
-
|
259
|
-
locators << { strategy: '
|
260
|
-
end
|
261
|
-
|
262
|
-
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
|
263
|
-
locators << { strategy: 'accessibility_id', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\"]" }
|
223
|
+
if attrs['name'] && !attrs['name'].empty?
|
224
|
+
# CORREÇÃO: A estratégia para 'name' no iOS é 'name', e o valor é o próprio nome.
|
225
|
+
locators << { strategy: 'name', locator: attrs['name'] }
|
264
226
|
end
|
265
|
-
|
266
227
|
if attrs['label'] && !attrs['label'].empty?
|
267
|
-
locators << { strategy: '
|
268
|
-
end
|
269
|
-
if attrs['name'] && !attrs['name'].empty?
|
270
|
-
locators << { strategy: 'name', locator: "//#{tag}[@name=\"#{self.truncate(attrs['name'])}\"]" }
|
228
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{truncate(attrs['label'])}\"]" }
|
271
229
|
end
|
272
|
-
|
273
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
274
|
-
|
275
230
|
locators
|
276
231
|
end
|
277
|
-
|
232
|
+
|
278
233
|
def self.generate_unknown_xpaths(tag, attrs)
|
279
|
-
|
280
|
-
|
281
|
-
locators
|
282
|
-
end
|
283
|
-
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
284
|
-
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
285
|
-
end
|
286
|
-
if attrs['text'] && !attrs['text'].empty?
|
287
|
-
locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
|
288
|
-
end
|
289
|
-
|
290
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
291
|
-
|
292
|
-
locators
|
234
|
+
locators = []
|
235
|
+
attrs.each { |key, value| locators << { strategy: key.to_s, locator: "//#{tag}[@#{key}=\"#{truncate(value)}\"]" } if value.is_a?(String) && !value.empty? }
|
236
|
+
locators
|
293
237
|
end
|
294
238
|
|
295
239
|
def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
|
296
|
-
|
240
|
+
# Lambdas para gerar partes do HTML
|
297
241
|
locators_html = lambda do |locators|
|
298
|
-
locators.map
|
299
|
-
"<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'>#{loc[:strategy].upcase.gsub('_', ' ')}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{loc[:locator]}</span></li>"
|
300
|
-
end.join
|
242
|
+
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
|
301
243
|
end
|
302
244
|
|
303
245
|
all_elements_html = lambda do |elements|
|
304
|
-
elements.map
|
305
|
-
"<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{el[:name]}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>"
|
306
|
-
end.join
|
246
|
+
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
|
307
247
|
end
|
308
248
|
|
249
|
+
# Prepara o conteúdo dinâmico
|
309
250
|
failed_info = targeted_report[:failed_element]
|
310
251
|
similar_elements = targeted_report[:similar_elements]
|
311
|
-
|
312
|
-
|
313
|
-
|
252
|
+
de_para_analysis = targeted_report[:de_para_analysis]
|
253
|
+
|
254
|
+
# Bloco de HTML para a análise "De/Para"
|
255
|
+
de_para_html = ""
|
256
|
+
if de_para_analysis
|
257
|
+
de_para_html = <<~HTML
|
258
|
+
<div class="bg-green-50 border border-green-200 p-4 rounded-lg shadow-md mb-6">
|
259
|
+
<h3 class="text-lg font-bold text-green-800 mb-2">Análise de Mapeamento (De/Para)</h3>
|
260
|
+
<p class="text-sm text-gray-700 mb-1">O nome lógico <strong class="font-mono bg-gray-200 px-1 rounded">#{CGI.escapeHTML(de_para_analysis[:logical_name])}</strong> foi encontrado nos seus arquivos locais!</p>
|
261
|
+
<p class="text-sm text-gray-700">O localizador correto definido é:</p>
|
262
|
+
<div class="font-mono text-xs bg-green-100 p-2 mt-2 rounded">
|
263
|
+
<span class="font-bold">#{CGI.escapeHTML(de_para_analysis[:correct_locator]['tipoBusca'].upcase)}:</span>
|
264
|
+
<span class="break-all">#{CGI.escapeHTML(de_para_analysis[:correct_locator]['valor'])}</span>
|
265
|
+
</div>
|
266
|
+
</div>
|
267
|
+
HTML
|
268
|
+
end
|
269
|
+
|
270
|
+
similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>" : similar_elements.map { |el|
|
271
|
+
score_percent = (el[:score] * 100).round(1)
|
272
|
+
"<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>"
|
273
|
+
}.join
|
274
|
+
|
314
275
|
failed_info_content = if failed_info && failed_info[:selector_value]
|
315
|
-
"<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'>#{failed_info[:selector_type]}</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'>#{failed_info[:selector_value]}</span></p>"
|
276
|
+
"<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>"
|
316
277
|
else
|
317
278
|
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
|
318
279
|
end
|
319
280
|
|
320
|
-
# Template HTML
|
281
|
+
# Template HTML completo
|
321
282
|
<<~HTML_REPORT
|
322
283
|
<!DOCTYPE html>
|
323
284
|
<html lang="pt-BR">
|
324
285
|
<head>
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
331
|
-
.tab-content { display: none; }
|
332
|
-
.tab-content.active { display: block; }
|
333
|
-
.tab-button.active { background-color: #4f46e5; color: white; }
|
334
|
-
.tab-button:not(.active):hover { background-color: #e0e7ff; }
|
335
|
-
</style>
|
286
|
+
<meta charset="UTF-8">
|
287
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
288
|
+
<title>Relatório de Falha Appium - #{timestamp}</title>
|
289
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
290
|
+
<style> body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; } .tab-content { display: none; } .tab-content.active { display: block; } .tab-button.active { background-color: #4f46e5; color: white; } </style>
|
336
291
|
</head>
|
337
292
|
<body class="bg-gray-50 p-8">
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
293
|
+
<div class="max-w-7xl mx-auto">
|
294
|
+
<header class="mb-8 pb-4 border-b border-gray-300">
|
295
|
+
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
296
|
+
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
|
297
|
+
</header>
|
298
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
299
|
+
<div class="lg:col-span-1">
|
300
|
+
#{de_para_html}
|
301
|
+
<div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
|
302
|
+
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
303
|
+
#{failed_info_content}
|
304
|
+
</div>
|
305
|
+
<div class="bg-white p-4 rounded-lg shadow-xl">
|
306
|
+
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
307
|
+
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
308
|
+
</div>
|
309
|
+
</div>
|
310
|
+
<div class="lg:col-span-2">
|
311
|
+
<div class="bg-white rounded-lg shadow-xl">
|
312
|
+
<div class="flex border-b border-gray-200">
|
313
|
+
<button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{similar_elements.size})</button>
|
314
|
+
<button class="tab-button px-4 py-3 text-sm font-medium text-gray-600" data-tab="all">Dump Completo da Página (#{all_suggestions.size} Elementos)</button>
|
315
|
+
</div>
|
316
|
+
<div class="p-6">
|
317
|
+
<div id="similar" class="tab-content active">
|
318
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
|
319
|
+
<div class="space-y-4">#{similar_elements_content}</div>
|
356
320
|
</div>
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
<div class="bg-white rounded-lg shadow-xl">
|
361
|
-
<!-- Abas de Navegação -->
|
362
|
-
<div class="flex border-b border-gray-200">
|
363
|
-
<button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{similar_elements.size})</button>
|
364
|
-
<button class="tab-button px-4 py-3 text-sm font-medium text-gray-600" data-tab="all">Dump Completo da Página (#{all_suggestions.size} Elementos)</button>
|
365
|
-
</div>
|
366
|
-
|
367
|
-
<!-- Conteúdo das Abas -->
|
368
|
-
<div class="p-6">
|
369
|
-
<!-- Aba Sugestões de Reparo -->
|
370
|
-
<div id="similar" class="tab-content active">
|
371
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
|
372
|
-
<div class="space-y-4">
|
373
|
-
#{similar_elements_content}
|
374
|
-
</div>
|
375
|
-
</div>
|
376
|
-
|
377
|
-
<!-- Aba Dump Completo -->
|
378
|
-
<div id="all" class="tab-content">
|
379
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
|
380
|
-
<div class="max-h-[600px] overflow-y-auto space-y-2">
|
381
|
-
#{all_elements_html.call(all_suggestions)}
|
382
|
-
</div>
|
383
|
-
</div>
|
384
|
-
</div>
|
385
|
-
</div>
|
321
|
+
<div id="all" class="tab-content">
|
322
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump de Todos os Elementos da Tela</h3>
|
323
|
+
<div class="max-h-[600px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
|
386
324
|
</div>
|
325
|
+
</div>
|
387
326
|
</div>
|
327
|
+
</div>
|
388
328
|
</div>
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
t.classList.add('text-gray-600');
|
402
|
-
});
|
403
|
-
contents.forEach(c => c.classList.remove('active'));
|
404
|
-
|
405
|
-
tab.classList.add('active', 'text-white', 'bg-indigo-600');
|
406
|
-
tab.classList.remove('text-gray-600');
|
407
|
-
document.getElementById(target).classList.add('active');
|
408
|
-
});
|
409
|
-
});
|
410
|
-
|
411
|
-
// Set initial active state for styling consistency
|
412
|
-
const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
|
413
|
-
activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
|
414
|
-
});
|
415
|
-
</script>
|
329
|
+
</div>
|
330
|
+
<script>
|
331
|
+
document.querySelectorAll('.tab-button').forEach(tab => {
|
332
|
+
tab.addEventListener('click', () => {
|
333
|
+
const target = tab.getAttribute('data-tab');
|
334
|
+
document.querySelectorAll('.tab-button').forEach(t => t.classList.remove('active'));
|
335
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
336
|
+
tab.classList.add('active');
|
337
|
+
document.getElementById(target).classList.add('active');
|
338
|
+
});
|
339
|
+
});
|
340
|
+
</script>
|
416
341
|
</body>
|
417
342
|
</html>
|
418
343
|
HTML_REPORT
|
419
344
|
end
|
420
345
|
end
|
421
|
-
end
|
346
|
+
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: 0.6.
|
4
|
+
version: 0.6.2
|
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-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|