appium_failure_helper 1.16.0 → 1.16.1

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