appium_failure_helper 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +16 -10
- data/lib/appium_failure_helper/handler.rb +59 -68
- data/lib/appium_failure_helper/report_generator.rb +71 -43
- 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: 770d3e03d633dae518f28754f37eb19163ef42ccd8012d873916774401124d5c
|
|
4
|
+
data.tar.gz: 865dd13a0253d937c45369bfe1f381e39c4b2181cdc37b9288a74fc0eecfa84f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6025a20bf43876d2ec6dc583ccd27a5ca4d410d3d129d14fab312ccd2fc7e42196e6366259a5bed90e85dca12fc640c10125408dad1aad3561b73de554dce1bc
|
|
7
|
+
data.tar.gz: 2b135ab6833e59378de96114926e7d61828093448b449053099630c59fe31167117abf6c2b1f40faf0d36ce452f672fa53dd4af2e749065f03f6a2df2ec45c0a
|
data/README.md
CHANGED
|
@@ -73,24 +73,30 @@ Ajuste seus métodos de busca de elementos (ex: em `features/support/appiumCusto
|
|
|
73
73
|
# --- MÉTODO DE ESPERA ENRIQUECIDO ---
|
|
74
74
|
def waitForElementExist(el, timeout = 30)
|
|
75
75
|
wait = Selenium::WebDriver::Wait.new(timeout: timeout)
|
|
76
|
+
|
|
76
77
|
begin
|
|
77
|
-
wait.until
|
|
78
|
+
wait.until do
|
|
79
|
+
if el.is_a?(Hash)
|
|
80
|
+
$driver.find_elements(el.keys.first, el.values.first).size > 0
|
|
81
|
+
else
|
|
82
|
+
# assume que é um ID simples (string)
|
|
83
|
+
$driver.find_elements(:id, el).size > 0
|
|
84
|
+
end
|
|
85
|
+
end
|
|
78
86
|
rescue Selenium::WebDriver::Error::TimeoutError => e
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
new_exception = e.class.new(new_message)
|
|
82
|
-
new_exception.set_backtrace(e.backtrace) # Preserva o stack trace
|
|
83
|
-
raise new_exception
|
|
87
|
+
locator_info = el.is_a?(Hash) ? "#{el.keys.first}: #{el.values.first}" : "using \"#{el}\""
|
|
88
|
+
raise e.class, "Timeout de #{timeout}s esperando pelo elemento (#{locator_info})", e.backtrace
|
|
84
89
|
end
|
|
85
90
|
end
|
|
86
91
|
|
|
92
|
+
|
|
87
93
|
# --- MÉTODO DE BUSCA ENRIQUECIDO ---
|
|
88
94
|
def find(el)
|
|
89
|
-
|
|
95
|
+
waitForElementExist(el)
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
def clickElement(el)
|
|
93
|
-
|
|
99
|
+
waitForElementExist(el).click
|
|
94
100
|
end
|
|
95
101
|
|
|
96
102
|
private
|
|
@@ -98,9 +104,9 @@ private
|
|
|
98
104
|
# Helper central que enriquece erros de 'find_element'
|
|
99
105
|
def find_element_with_enriched_error(el)
|
|
100
106
|
begin
|
|
101
|
-
return $driver.find_element(el
|
|
107
|
+
return $driver.find_element(el)
|
|
102
108
|
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
|
103
|
-
new_message = "using \"#{el
|
|
109
|
+
new_message = "using \"#{el.keys.first}\""
|
|
104
110
|
new_exception = e.class.new(new_message)
|
|
105
111
|
new_exception.set_backtrace(e.backtrace)
|
|
106
112
|
raise new_exception
|
|
@@ -32,56 +32,71 @@ module AppiumFailureHelper
|
|
|
32
32
|
screenshot_base_64: safe_screenshot_base64
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
all_page_elements = []
|
|
48
|
-
best_candidate_analysis = nil
|
|
49
|
-
alternative_xpaths = []
|
|
50
|
-
|
|
51
|
-
if page_source && !failed_info.empty?
|
|
52
|
-
tag_for_factory = nil
|
|
53
|
-
attrs_for_factory = nil
|
|
54
|
-
|
|
55
|
-
if best_candidate_analysis && (attrs = best_candidate_analysis[:attributes])
|
|
56
|
-
# Se encontrou um candidato, usa os atributos dele
|
|
57
|
-
tag_for_factory = attrs['tag']
|
|
58
|
-
attrs_for_factory = attrs
|
|
35
|
+
page_source = safe_page_source
|
|
36
|
+
failed_info = fetch_failed_element || {}
|
|
37
|
+
|
|
38
|
+
# Extrai todos os elementos da tela
|
|
39
|
+
all_page_elements = []
|
|
40
|
+
if page_source
|
|
41
|
+
begin
|
|
42
|
+
if defined?(DumpParser) && DumpParser.respond_to?(:parse)
|
|
43
|
+
all_page_elements = DumpParser.parse(page_source) || []
|
|
44
|
+
elsif defined?(Dump) && Dump.respond_to?(:from_xml)
|
|
45
|
+
all_page_elements = Dump.from_xml(page_source) || []
|
|
59
46
|
else
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
47
|
+
require 'nokogiri' unless defined?(Nokogiri)
|
|
48
|
+
doc = Nokogiri::XML(page_source) rescue nil
|
|
49
|
+
if doc
|
|
50
|
+
all_page_elements = doc.xpath('//*').map do |node|
|
|
51
|
+
{ name: node.name, attributes: node.attributes.transform_values(&:value).merge('tag' => node.name) }
|
|
52
|
+
end
|
|
65
53
|
end
|
|
66
54
|
end
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# -----------------------------------------------
|
|
55
|
+
rescue => e
|
|
56
|
+
Utils.logger.warn("Falha ao extrair elementos do page_source: #{e.message}")
|
|
57
|
+
all_page_elements = []
|
|
71
58
|
end
|
|
59
|
+
end
|
|
72
60
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
# Executa análise avançada se houver failed_info
|
|
62
|
+
best_candidate_analysis = if failed_info.empty?
|
|
63
|
+
# Fallback: gerar análise heurística mesmo sem failed_info
|
|
64
|
+
all_page_elements.map do |elm|
|
|
65
|
+
{
|
|
66
|
+
score: 0,
|
|
67
|
+
name: elm[:name],
|
|
68
|
+
attributes: elm[:attributes],
|
|
69
|
+
analysis: {}
|
|
70
|
+
}
|
|
71
|
+
end.first(3)
|
|
72
|
+
else
|
|
73
|
+
Analyzer.perform_advanced_analysis(failed_info, all_page_elements, platform) rescue nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Gera alternativas de XPath mesmo sem failed_info
|
|
77
|
+
tag_for_factory = nil
|
|
78
|
+
attrs_for_factory = nil
|
|
79
|
+
if best_candidate_analysis&.any?
|
|
80
|
+
first_candidate = best_candidate_analysis.first
|
|
81
|
+
attrs = first_candidate[:attributes] || {}
|
|
82
|
+
tag_for_factory = attrs['tag'] || first_candidate[:name]
|
|
83
|
+
attrs_for_factory = attrs
|
|
80
84
|
end
|
|
85
|
+
alternative_xpaths = XPathFactory.generate_for_node(tag_for_factory, attrs_for_factory) if tag_for_factory && attrs_for_factory
|
|
86
|
+
|
|
87
|
+
report_data.merge!(
|
|
88
|
+
page_source: page_source,
|
|
89
|
+
failed_element: failed_info,
|
|
90
|
+
best_candidate_analysis: best_candidate_analysis,
|
|
91
|
+
alternative_xpaths: alternative_xpaths,
|
|
92
|
+
all_page_elements: all_page_elements
|
|
93
|
+
)
|
|
81
94
|
|
|
95
|
+
# Gera relatório
|
|
82
96
|
report_generator = ReportGenerator.new(@output_folder, report_data)
|
|
83
97
|
generated_html_path = report_generator.generate_all
|
|
84
98
|
copy_report_for_ci(generated_html_path)
|
|
99
|
+
|
|
85
100
|
Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
|
|
86
101
|
|
|
87
102
|
rescue => e
|
|
@@ -93,31 +108,27 @@ module AppiumFailureHelper
|
|
|
93
108
|
|
|
94
109
|
def safe_screenshot_base64
|
|
95
110
|
@driver.respond_to?(:screenshot_as) ? @driver.screenshot_as(:base64) : nil
|
|
96
|
-
rescue
|
|
111
|
+
rescue
|
|
97
112
|
nil
|
|
98
113
|
end
|
|
99
114
|
|
|
100
115
|
def safe_page_source
|
|
101
116
|
return nil unless @driver.respond_to?(:page_source)
|
|
102
117
|
@driver.page_source
|
|
103
|
-
rescue
|
|
118
|
+
rescue
|
|
104
119
|
nil
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
def fetch_failed_element
|
|
108
123
|
msg = @exception&.message.to_s
|
|
109
124
|
|
|
110
|
-
# 1) tentativa de parse clássico com aspas (mais restritivo)
|
|
111
125
|
if (m = msg.match(/using\s+['"](?<type>[^'"]+)['"]\s+with\s+value\s+['"](?<value>.*?)['"]/m))
|
|
112
126
|
return { selector_type: m[:type], selector_value: m[:value] }
|
|
113
127
|
end
|
|
114
128
|
|
|
115
|
-
# 2) fallback: pega anything após 'with value' até o final da linha (remove quotes extras)
|
|
116
129
|
if (m = msg.match(/with\s+value\s+(?<value>.+)$/mi))
|
|
117
130
|
raw = m[:value].strip
|
|
118
|
-
# remove quotes de borda apenas se existirem
|
|
119
131
|
raw = raw[1..-2] if raw.start_with?('"', "'") && raw.end_with?('"', "'")
|
|
120
|
-
# tenta detectar o tipo (xpath, id, accessibility id, css)
|
|
121
132
|
guessed_type = if raw =~ %r{^//|^/}i
|
|
122
133
|
'xpath'
|
|
123
134
|
elsif raw =~ /^[a-zA-Z0-9\-_:.]+:/
|
|
@@ -128,46 +139,26 @@ module AppiumFailureHelper
|
|
|
128
139
|
return { selector_type: guessed_type, selector_value: raw }
|
|
129
140
|
end
|
|
130
141
|
|
|
131
|
-
# 3) outros formatos JSON-like
|
|
132
142
|
if (m = msg.match(/"method"\s*:\s*"([^"]+)"[\s,}].*"selector"\s*:\s*"([^"]+)"/i))
|
|
133
143
|
return { selector_type: m[1], selector_value: m[2] }
|
|
134
144
|
end
|
|
135
145
|
|
|
136
|
-
# 4) tentativa simples: pegar primeira ocorrência entre aspas
|
|
137
146
|
if (m = msg.match(/["']([^"']+)["']/))
|
|
138
147
|
maybe_value = m[1]
|
|
139
|
-
guessed_type = msg[/\b(xpath|id|accessibility id|css)\b/i] ? $&.downcase :
|
|
148
|
+
guessed_type = msg[/\b(xpath|id|accessibility id|css)\b/i] ? $&.downcase : 'unknown'
|
|
140
149
|
return { selector_type: guessed_type || 'unknown', selector_value: maybe_value }
|
|
141
150
|
end
|
|
142
151
|
|
|
143
152
|
{}
|
|
144
153
|
end
|
|
145
154
|
|
|
146
|
-
def parse_attrs_from_locator_string(selector_value)
|
|
147
|
-
attrs = {}
|
|
148
|
-
return attrs unless selector_value.is_a?(String) && !selector_value.empty?
|
|
149
|
-
|
|
150
|
-
selector_value.scan(/@([a-zA-Z0-9\-\:]+)\s*=\s*['"]([^'"]+)['"]/).each do |k, v|
|
|
151
|
-
attrs[k] = v
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
if selector_value =~ %r{//\s*([a-zA-Z0-9_\-:]+)}
|
|
155
|
-
attrs['tag'] = $1
|
|
156
|
-
elsif selector_value =~ /^([a-zA-Z0-9_\-:]+)\[/
|
|
157
|
-
attrs['tag'] = $1
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
attrs
|
|
161
|
-
end
|
|
162
|
-
|
|
163
155
|
def copy_report_for_ci(source_html_path)
|
|
164
156
|
return unless source_html_path && File.exist?(source_html_path)
|
|
165
157
|
|
|
166
158
|
ci_report_dir = File.join(Dir.pwd, 'ci_failure_report')
|
|
167
159
|
FileUtils.mkdir_p(ci_report_dir)
|
|
168
|
-
|
|
160
|
+
|
|
169
161
|
destination_path = File.join(ci_report_dir, 'index.html')
|
|
170
|
-
|
|
171
162
|
FileUtils.cp(source_html_path, destination_path)
|
|
172
163
|
Utils.logger.info("Relatório copiado para CI em: #{destination_path}")
|
|
173
164
|
rescue => e
|
|
@@ -2,8 +2,8 @@ module AppiumFailureHelper
|
|
|
2
2
|
class ReportGenerator
|
|
3
3
|
def initialize(output_folder, report_data)
|
|
4
4
|
@output_folder = output_folder
|
|
5
|
-
@data = report_data
|
|
6
|
-
@
|
|
5
|
+
@data = report_data.transform_keys(&:to_sym) rescue report_data
|
|
6
|
+
@dump = @data[:dump] || @data['dump']
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def generate_all
|
|
@@ -13,11 +13,21 @@ module AppiumFailureHelper
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
private
|
|
16
|
+
|
|
17
|
+
def safe_escape_html(value)
|
|
18
|
+
CGI.escapeHTML(value.to_s)
|
|
19
|
+
end
|
|
16
20
|
|
|
17
21
|
def generate_xml_report
|
|
18
|
-
|
|
22
|
+
FileUtils.mkdir_p(@output_folder) unless Dir.exist?(@output_folder)
|
|
23
|
+
if @page_source && !@page_source.empty?
|
|
24
|
+
File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
|
|
25
|
+
else
|
|
26
|
+
puts "⚠️ Page source está vazio, XML não será gerado"
|
|
27
|
+
end
|
|
19
28
|
end
|
|
20
29
|
|
|
30
|
+
|
|
21
31
|
def generate_yaml_reports
|
|
22
32
|
analysis_report = {
|
|
23
33
|
triage_result: @data[:triage_result],
|
|
@@ -43,68 +53,86 @@ module AppiumFailureHelper
|
|
|
43
53
|
message: "A análise profunda do seletor não foi executada ou falhou. Verifique a mensagem de erro original e o stack trace."
|
|
44
54
|
)
|
|
45
55
|
end
|
|
56
|
+
|
|
46
57
|
html_file_path = File.join(@output_folder, "report_#{@data[:timestamp]}.html")
|
|
47
58
|
File.write(html_file_path, html_content)
|
|
48
|
-
|
|
49
|
-
return html_file_path
|
|
59
|
+
html_file_path
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
def build_full_report
|
|
53
63
|
failed_info = @data[:failed_element] || {}
|
|
54
64
|
all_suggestions = @data[:all_page_elements] || []
|
|
55
|
-
best_candidate = @data[:best_candidate_analysis]
|
|
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
|
|
56
74
|
alternative_xpaths = @data[:alternative_xpaths] || []
|
|
57
75
|
timestamp = @data[:timestamp]
|
|
58
76
|
platform = @data[:platform]
|
|
59
77
|
screenshot_base64 = @data[:screenshot_base_64]
|
|
60
78
|
|
|
61
79
|
locators_html = lambda do |locators|
|
|
62
|
-
(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'>#{
|
|
80
|
+
(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'>#{safe_escape_html(loc[:strategy].to_s.upcase.gsub('_', ' '))}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{safe_escape_html(loc[:locator])}</span></li>" }.join
|
|
63
81
|
end
|
|
64
82
|
|
|
65
83
|
all_elements_html = lambda do |elements|
|
|
66
|
-
(elements || []).map { |el| "<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{
|
|
84
|
+
(elements || []).map { |el| "<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{safe_escape_html(el[:name])}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>" }.join
|
|
67
85
|
end
|
|
68
86
|
|
|
69
|
-
failed_info_content = "<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'>#{
|
|
87
|
+
failed_info_content = "<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].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-words'>#{safe_escape_html(failed_info[:selector_value].to_s)}</span></p>"
|
|
70
88
|
|
|
71
89
|
advanced_analysis_html = if best_candidate.nil?
|
|
72
90
|
"<p class='text-gray-500'>Nenhum candidato provável foi encontrado na tela atual para uma análise detalhada.</p>"
|
|
73
91
|
else
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
analysis_details = (best_candidate[:analysis] || {}).map do |key, data|
|
|
93
|
+
data ||= {} # garante que não seja nil
|
|
94
|
+
status_color = 'bg-gray-400'
|
|
95
|
+
status_icon = '⚪'
|
|
96
|
+
|
|
97
|
+
expected = data[:expected].to_s
|
|
98
|
+
actual = data[:actual].to_s
|
|
99
|
+
similarity = data[:similarity].to_f
|
|
100
|
+
match = data[:match]
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
104
132
|
|
|
105
133
|
<<~HTML
|
|
106
134
|
<div class='border border-sky-200 bg-sky-50 p-4 rounded-lg'>
|
|
107
|
-
<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'>#{
|
|
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>
|
|
108
136
|
<ul class='space-y-2 mb-4'>#{analysis_details}</ul>
|
|
109
137
|
<div class='bg-sky-100 border-l-4 border-sky-500 text-sky-900 text-sm p-3 rounded-r-lg'>
|
|
110
138
|
<p><b>Sugestão:</b> #{suggestion_text}</p>
|
|
@@ -128,12 +156,12 @@ module AppiumFailureHelper
|
|
|
128
156
|
<<~STRATEGY_ITEM
|
|
129
157
|
<li class='border-b border-gray-200 py-3 last:border-b-0'>
|
|
130
158
|
<div class='flex justify-between items-center mb-1'>
|
|
131
|
-
<p class='font-semibold text-indigo-800 text-sm'>#{
|
|
132
|
-
<span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{
|
|
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>
|
|
133
161
|
</div>
|
|
134
162
|
<div class='bg-gray-800 text-white p-2 rounded mt-1 text-xs whitespace-pre-wrap break-words font-mono'>
|
|
135
|
-
<span class='font-bold text-indigo-400'>#{
|
|
136
|
-
<code class='ml-1'>#{
|
|
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>
|
|
137
165
|
</div>
|
|
138
166
|
</li>
|
|
139
167
|
STRATEGY_ITEM
|
|
@@ -279,8 +307,8 @@ module AppiumFailureHelper
|
|
|
279
307
|
def build_simple_diagnosis_report(title:, message:)
|
|
280
308
|
exception = @data[:exception]
|
|
281
309
|
screenshot = @data[:screenshot_base_64]
|
|
282
|
-
error_message_html =
|
|
283
|
-
backtrace_html =
|
|
310
|
+
error_message_html = safe_escape_html(exception.message.to_s)
|
|
311
|
+
backtrace_html = safe_escape_html(exception.backtrace.join("\n"))
|
|
284
312
|
|
|
285
313
|
<<~HTML_REPORT
|
|
286
314
|
<!DOCTYPE html>
|