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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6693ea274ba569af7ec3440dcd29348769a3860920dc6b88f35b0d543c145183
4
- data.tar.gz: 416b624ae5218f624d817c488498068237237fd52dca0b51aa603f488cc59c46
3
+ metadata.gz: 770d3e03d633dae518f28754f37eb19163ef42ccd8012d873916774401124d5c
4
+ data.tar.gz: 865dd13a0253d937c45369bfe1f381e39c4b2181cdc37b9288a74fc0eecfa84f
5
5
  SHA512:
6
- metadata.gz: b2140d8b30afd01b22e5f23960efab1f7654c01727efce1e8f3687e4a4a10a76c589c083a7ee71dd79b95b0b4ee925b51bc9694d835789288a112052fec54a10
7
- data.tar.gz: cee9beef7f24c1b839ab054861fcb37e2c681ba2140a36631f7d62158d7eb9c7526fe7c6db68a30406c05f86d16372950779b06d1037c884313cff1ac88ae1da
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 { $driver.find_elements(el['tipoBusca'], el['value']).size > 0 }
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
- # CRUCIAL: Relança o erro com uma mensagem explícita que a GEM entende.
80
- new_message = "Timeout de #{timeout}s esperando pelo elemento: using \"#{el['tipoBusca']}\" with value \"#{el['value']}\""
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
- find_element_with_enriched_error(el)
95
+ waitForElementExist(el)
90
96
  end
91
97
 
92
98
  def clickElement(el)
93
- find_element_with_enriched_error(el).click
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['tipoBusca'], el['value'])
107
+ return $driver.find_element(el)
102
108
  rescue Selenium::WebDriver::Error::NoSuchElementError => e
103
- new_message = "using \"#{el['tipoBusca']}\" with value \"#{el['value']}\""
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
- if triage_result == :locator_issue
36
- page_source = safe_page_source
37
- failed_info = fetch_failed_element || {}
38
-
39
- if failed_info.empty? && SourceCodeAnalyzer.respond_to?(:extract_from_exception)
40
- failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
41
- end
42
-
43
- if failed_info.empty?
44
- report_data[:triage_result] = :unidentified_locator_issue
45
- end
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
- # Se NÃO encontrou, usa os atributos do seletor que falhou
61
- failed_attrs = parse_attrs_from_locator_string(failed_info[:selector_value] || '')
62
- if !failed_attrs.empty?
63
- tag_for_factory = failed_attrs.delete('tag')
64
- attrs_for_factory = failed_attrs
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
- # Gera as estratégias usando apenas tag e atributos
69
- alternative_xpaths = XPathFactory.generate_for_node(tag_for_factory, attrs_for_factory) if tag_for_factory && attrs_for_factory
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
- report_data.merge!({
74
- page_source: page_source,
75
- failed_element: failed_info,
76
- best_candidate_analysis: best_candidate_analysis,
77
- alternative_xpaths: alternative_xpaths,
78
- all_page_elements: all_page_elements
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 : nil
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
- @page_source = report_data[:page_source]
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
- File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
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'>#{CGI.escapeHTML(loc[:strategy].to_s.upcase.gsub('_', ' '))}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>" }.join
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'>#{CGI.escapeHTML(el[:name])}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>" }.join
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'>#{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-words'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
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
- analysis_details = (best_candidate[:analysis] || {}).map do |key, data|
75
- status_color = 'bg-gray-400'
76
- status_icon = ''
77
- status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Não verificado</span>"
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
- if data[:match] == true || (data[:similarity] && data[:similarity] == 1.0)
80
- status_color = 'bg-green-500'
81
- status_icon = '✅'
82
- status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Correspondência Exata!</span>"
83
- elsif data[:similarity] && data[:similarity] > 0.7
84
- status_color = 'bg-yellow-500'
85
- status_icon = '⚠️'
86
- status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Parecido (Encontrado: '#{CGI.escapeHTML(data[:actual])}')</span>"
87
- else
88
- status_color = 'bg-red-500'
89
- status_icon = '❌'
90
- status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Diferente! Esperado: '#{CGI.escapeHTML(data[:expected].to_s)}'</span>"
91
- end
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
- "<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>"
94
- end.join
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
- suggestion_text = "O `resource-id` pode ter mudado ou o `text` está diferente. Considere usar um seletor mais robusto baseado nos atributos que corresponderam."
97
- if (best_candidate[:analysis][:id] || {})[:match] == true && (best_candidate[:analysis][:text] || {})[:similarity].to_f < 0.7
98
- suggestion_text = "Prefira usar atributos estáveis, como resource-id ou content-desc.",
99
- "Evite caminhos absolutos (//hierarchy/...); prefira XPaths curtos e relativos.",
100
- "Use normalize-space() para lidar com espaços e maiúsculas/minúsculas.",
101
- "Combine atributos, ex: //*[@resource-id='id' and @text='Texto'].",
102
- "Evite localizar por texto dinâmico, prefira IDs únicos."
103
- end
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'>#{CGI.escapeHTML(best_candidate[:name])}</span></h4>
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'>#{CGI.escapeHTML(strategy[:name])}</p>
132
- <span class='text-xs font-medium px-2 py-0.5 rounded-full #{reliability_color}'>#{CGI.escapeHTML(strategy[:reliability].to_s.capitalize)}</span>
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'>#{CGI.escapeHTML(strategy[:strategy].to_s.upcase)}:</span>
136
- <code class='ml-1'>#{CGI.escapeHTML(strategy[:locator])}</code>
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 = CGI.escapeHTML(exception.message.to_s)
283
- backtrace_html = CGI.escapeHTML(exception.backtrace.join("\n"))
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>
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.15.0"
2
+ VERSION = "1.16.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appium_failure_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.15.0
4
+ version: 1.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento