appium_failure_helper 0.6.1 → 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 +218 -338
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 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,233 +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
|
-
FileUtils.rm_rf("reports_failure")
|
43
|
-
@@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
|
44
|
-
|
45
33
|
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
46
34
|
output_folder = "reports_failure/failure_#{timestamp}"
|
47
|
-
|
48
35
|
FileUtils.mkdir_p(output_folder)
|
49
36
|
@@logger.info("Pasta de saída criada: #{output_folder}")
|
50
|
-
|
51
37
|
screenshot_base64 = driver.screenshot_as(:base64)
|
52
|
-
screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
|
53
|
-
File.open(screenshot_path, 'wb') do |f|
|
54
|
-
f.write(Base64.decode64(screenshot_base64))
|
55
|
-
end
|
56
|
-
@@logger.info("Screenshot salvo em #{screenshot_path}")
|
57
|
-
|
58
38
|
page_source = driver.page_source
|
59
|
-
|
60
|
-
File.write(xml_path, page_source)
|
61
|
-
@@logger.info("Page source salvo em #{xml_path}")
|
62
|
-
|
39
|
+
File.write("#{output_folder}/page_source_#{timestamp}.xml", page_source)
|
63
40
|
doc = Nokogiri::XML(page_source)
|
64
41
|
platform = driver.capabilities['platformName']&.downcase || 'unknown'
|
65
|
-
|
66
42
|
failed_element_info = self.extract_info_from_exception(exception)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
unless seen_elements[unique_key]
|
77
|
-
name = self.suggest_name(node.name, attrs)
|
78
|
-
locators = self.xpath_generator(node.name, attrs, platform)
|
79
|
-
|
80
|
-
all_elements_suggestions << { name: name, locators: locators }
|
81
|
-
seen_elements[unique_key] = true
|
82
|
-
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
|
+
}
|
83
51
|
end
|
84
|
-
|
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)
|
85
54
|
targeted_report = {
|
86
55
|
failed_element: failed_element_info,
|
87
|
-
similar_elements:
|
56
|
+
similar_elements: similar_elements,
|
57
|
+
de_para_analysis: de_para_result
|
88
58
|
}
|
89
|
-
|
90
|
-
|
91
|
-
targeted_report[:similar_elements] = self.find_similar_elements(doc, failed_element_info, platform)
|
92
|
-
end
|
93
|
-
|
94
|
-
targeted_yaml_path = "#{output_folder}/failure_analysis_#{timestamp}.yaml"
|
95
|
-
File.open(targeted_yaml_path, 'w') do |f|
|
96
|
-
f.write(YAML.dump(targeted_report))
|
97
|
-
end
|
98
|
-
@@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
|
99
|
-
|
100
|
-
full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
|
101
|
-
File.open(full_dump_yaml_path, 'w') do |f|
|
102
|
-
f.write(YAML.dump(all_elements_suggestions))
|
103
|
-
end
|
104
|
-
@@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
|
105
|
-
|
106
|
-
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)) }
|
107
61
|
html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
|
108
|
-
File.write(
|
109
|
-
@@logger.info("
|
110
|
-
|
62
|
+
File.write("#{output_folder}/report_#{timestamp}.html", html_content)
|
63
|
+
@@logger.info("Relatórios gerados com sucesso em: #{output_folder}")
|
111
64
|
rescue => e
|
112
65
|
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
113
66
|
end
|
114
67
|
end
|
115
68
|
|
116
69
|
private
|
117
|
-
|
118
|
-
def self.setup_logger
|
119
|
-
@@logger ||= Logger.new(STDOUT)
|
120
|
-
@@logger.level = Logger::INFO
|
121
|
-
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
122
|
-
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def self.extract_info_from_exception(exception)
|
127
|
-
message = exception.to_s
|
128
|
-
info = {}
|
129
|
-
|
130
|
-
# normaliza tipos capturados para nomes previsíveis
|
131
|
-
normalize_type = lambda do |t|
|
132
|
-
return 'unknown' unless t
|
133
|
-
t = t.to_s.downcase.strip
|
134
|
-
t = t.gsub(/["']/, '')
|
135
|
-
case t
|
136
|
-
when 'cssselector', 'css selector' then 'css'
|
137
|
-
when 'classname', 'class name' then 'class name'
|
138
|
-
when 'accessibilityid', 'accessibility-id', 'accessibility id' then 'accessibility-id'
|
139
|
-
when 'resourceid', 'resource-id', 'resource id', 'id' then 'resource-id'
|
140
|
-
when 'contentdesc', 'content-desc', 'content desc' then 'content-desc'
|
141
|
-
else
|
142
|
-
t.gsub(/\s+/, '_').gsub(/[^a-z0-9_\-]/, '')
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
patterns = [
|
147
|
-
# ChromeDriver/Selenium JSON style:
|
148
|
-
# no such element: Unable to locate element: {"method":"xpath","selector":"//..."}
|
149
|
-
/no such element: Unable to locate element:\s*\{\s*["']?method["']?\s*:\s*["']?([^"'\}]+)["']?\s*,\s*["']?selector["']?\s*:\s*["']?([^"']+)["']?\s*\}/i,
|
150
|
-
|
151
|
-
# By.xpath: //..., By.id: "foo"
|
152
|
-
/By\.(xpath|id|css selector|cssSelector|name|class name|className):\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
153
70
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
patterns.each do |pattern|
|
171
|
-
if (m = message.match(pattern))
|
172
|
-
caps = m.captures.compact
|
173
|
-
if caps.length >= 2
|
174
|
-
raw_type = caps[0].to_s.strip
|
175
|
-
raw_value = caps[1].to_s.strip
|
176
|
-
info[:selector_type] = normalize_type.call(raw_type)
|
177
|
-
info[:selector_value] = raw_value.gsub(/\A['"]|['"]\z/, '')
|
178
|
-
else
|
179
|
-
info[:selector_type] = 'unknown'
|
180
|
-
info[:selector_value] = caps[0].to_s.strip.gsub(/\A['"]|['"]\z/, '')
|
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
|
181
86
|
end
|
182
|
-
info[:raw_message] = message[0, 1000]
|
183
|
-
return info
|
184
87
|
end
|
88
|
+
elements_map
|
185
89
|
end
|
186
90
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
info[:raw_message] = message[0,1000]
|
192
|
-
return info
|
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" }
|
193
95
|
end
|
194
96
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
97
|
+
def self.extract_info_from_exception(exception)
|
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
|
113
|
+
end
|
114
|
+
info
|
115
|
+
end
|
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
|
208
133
|
|
209
|
-
|
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?
|
210
138
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
attrs['text']&.downcase&.include?(selector_value) ||
|
215
|
-
attrs['content-desc']&.downcase&.include?(selector_value))
|
216
|
-
when 'ios'
|
217
|
-
(attrs['accessibility-id']&.downcase&.include?(selector_value) ||
|
218
|
-
attrs['label']&.downcase&.include?(selector_value) ||
|
219
|
-
attrs['name']&.downcase&.include?(selector_value))
|
220
|
-
else
|
221
|
-
false
|
222
|
-
end
|
223
|
-
|
224
|
-
if is_similar
|
225
|
-
name = self.suggest_name(node.name, attrs)
|
226
|
-
locators = self.xpath_generator(node.name, attrs, platform)
|
227
|
-
similar_elements << { name: name, locators: locators }
|
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 }
|
228
142
|
end
|
229
143
|
end
|
230
|
-
|
144
|
+
similarities.sort_by { |s| -s[:score] }.first(5)
|
231
145
|
end
|
232
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
|
+
|
233
165
|
def self.truncate(value)
|
234
166
|
return value unless value.is_a?(String)
|
235
167
|
value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
|
@@ -240,7 +172,13 @@ module AppiumFailureHelper
|
|
240
172
|
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
241
173
|
name_base = nil
|
242
174
|
|
243
|
-
|
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|
|
244
182
|
value = attrs[attr_key]
|
245
183
|
if value.is_a?(String) && !value.empty?
|
246
184
|
name_base = value
|
@@ -248,7 +186,8 @@ module AppiumFailureHelper
|
|
248
186
|
end
|
249
187
|
end
|
250
188
|
|
251
|
-
|
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
|
252
191
|
|
253
192
|
truncated_name = truncate(name_base)
|
254
193
|
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
@@ -258,209 +197,150 @@ module AppiumFailureHelper
|
|
258
197
|
|
259
198
|
def self.xpath_generator(tag, attrs, platform)
|
260
199
|
case platform
|
261
|
-
when 'android'
|
262
|
-
|
263
|
-
|
264
|
-
self.generate_ios_xpaths(tag, attrs)
|
265
|
-
else
|
266
|
-
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)
|
267
203
|
end
|
268
204
|
end
|
269
205
|
|
270
206
|
def self.generate_android_xpaths(tag, attrs)
|
271
207
|
locators = []
|
272
|
-
|
273
|
-
if attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['text'] && !attrs['text'].empty?
|
274
|
-
locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @text=\"#{self.truncate(attrs['text'])}\"]" }
|
275
|
-
elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
|
276
|
-
locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
277
|
-
end
|
278
|
-
|
279
208
|
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
280
|
-
|
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'] }
|
281
211
|
end
|
282
|
-
|
283
|
-
if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
|
284
|
-
id_part = attrs['resource-id'].split(':id/').last
|
285
|
-
locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, \"#{id_part}\")]" }
|
286
|
-
end
|
287
|
-
|
288
212
|
if attrs['text'] && !attrs['text'].empty?
|
289
|
-
locators << { strategy: '
|
213
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{truncate(attrs['text'])}\"]" }
|
290
214
|
end
|
291
215
|
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
292
|
-
locators << { strategy: '
|
216
|
+
locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{truncate(attrs['content-desc'])}\"]" }
|
293
217
|
end
|
294
|
-
|
295
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
296
|
-
|
297
218
|
locators
|
298
219
|
end
|
299
220
|
|
300
221
|
def self.generate_ios_xpaths(tag, attrs)
|
301
222
|
locators = []
|
302
|
-
|
303
|
-
|
304
|
-
locators << { strategy: '
|
305
|
-
end
|
306
|
-
|
307
|
-
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
|
308
|
-
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'] }
|
309
226
|
end
|
310
|
-
|
311
227
|
if attrs['label'] && !attrs['label'].empty?
|
312
|
-
locators << { strategy: '
|
228
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{truncate(attrs['label'])}\"]" }
|
313
229
|
end
|
314
|
-
if attrs['name'] && !attrs['name'].empty?
|
315
|
-
locators << { strategy: 'name', locator: "//#{tag}[@name=\"#{self.truncate(attrs['name'])}\"]" }
|
316
|
-
end
|
317
|
-
|
318
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
319
|
-
|
320
230
|
locators
|
321
231
|
end
|
322
|
-
|
232
|
+
|
323
233
|
def self.generate_unknown_xpaths(tag, attrs)
|
324
|
-
|
325
|
-
|
326
|
-
locators
|
327
|
-
end
|
328
|
-
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
329
|
-
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
330
|
-
end
|
331
|
-
if attrs['text'] && !attrs['text'].empty?
|
332
|
-
locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
|
333
|
-
end
|
334
|
-
|
335
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
336
|
-
|
337
|
-
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
|
338
237
|
end
|
339
238
|
|
340
239
|
def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
|
341
|
-
|
240
|
+
# Lambdas para gerar partes do HTML
|
342
241
|
locators_html = lambda do |locators|
|
343
|
-
locators.map
|
344
|
-
"<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>"
|
345
|
-
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
|
346
243
|
end
|
347
244
|
|
348
245
|
all_elements_html = lambda do |elements|
|
349
|
-
elements.map
|
350
|
-
"<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>"
|
351
|
-
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
|
352
247
|
end
|
353
248
|
|
249
|
+
# Prepara o conteúdo dinâmico
|
354
250
|
failed_info = targeted_report[:failed_element]
|
355
251
|
similar_elements = targeted_report[:similar_elements]
|
356
|
-
|
357
|
-
|
358
|
-
|
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
|
+
|
359
275
|
failed_info_content = if failed_info && failed_info[:selector_value]
|
360
|
-
"<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>"
|
361
277
|
else
|
362
278
|
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
|
363
279
|
end
|
364
280
|
|
365
|
-
# Template HTML
|
281
|
+
# Template HTML completo
|
366
282
|
<<~HTML_REPORT
|
367
283
|
<!DOCTYPE html>
|
368
284
|
<html lang="pt-BR">
|
369
285
|
<head>
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
376
|
-
.tab-content { display: none; }
|
377
|
-
.tab-content.active { display: block; }
|
378
|
-
.tab-button.active { background-color: #4f46e5; color: white; }
|
379
|
-
.tab-button:not(.active):hover { background-color: #e0e7ff; }
|
380
|
-
</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>
|
381
291
|
</head>
|
382
292
|
<body class="bg-gray-50 p-8">
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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>
|
401
320
|
</div>
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
<div class="bg-white rounded-lg shadow-xl">
|
406
|
-
<!-- Abas de Navegação -->
|
407
|
-
<div class="flex border-b border-gray-200">
|
408
|
-
<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>
|
409
|
-
<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>
|
410
|
-
</div>
|
411
|
-
|
412
|
-
<!-- Conteúdo das Abas -->
|
413
|
-
<div class="p-6">
|
414
|
-
<!-- Aba Sugestões de Reparo -->
|
415
|
-
<div id="similar" class="tab-content active">
|
416
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
|
417
|
-
<div class="space-y-4">
|
418
|
-
#{similar_elements_content}
|
419
|
-
</div>
|
420
|
-
</div>
|
421
|
-
|
422
|
-
<!-- Aba Dump Completo -->
|
423
|
-
<div id="all" class="tab-content">
|
424
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
|
425
|
-
<div class="max-h-[600px] overflow-y-auto space-y-2">
|
426
|
-
#{all_elements_html.call(all_suggestions)}
|
427
|
-
</div>
|
428
|
-
</div>
|
429
|
-
</div>
|
430
|
-
</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>
|
431
324
|
</div>
|
325
|
+
</div>
|
432
326
|
</div>
|
327
|
+
</div>
|
433
328
|
</div>
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
t.classList.add('text-gray-600');
|
447
|
-
});
|
448
|
-
contents.forEach(c => c.classList.remove('active'));
|
449
|
-
|
450
|
-
tab.classList.add('active', 'text-white', 'bg-indigo-600');
|
451
|
-
tab.classList.remove('text-gray-600');
|
452
|
-
document.getElementById(target).classList.add('active');
|
453
|
-
});
|
454
|
-
});
|
455
|
-
|
456
|
-
// Set initial active state for styling consistency
|
457
|
-
const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
|
458
|
-
activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
|
459
|
-
});
|
460
|
-
</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>
|
461
341
|
</body>
|
462
342
|
</html>
|
463
343
|
HTML_REPORT
|
464
344
|
end
|
465
345
|
end
|
466
|
-
end
|
346
|
+
end
|