appium_failure_helper 0.6.2 → 0.6.4
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/analyzer.rb +61 -0
- data/lib/appium_failure_helper/element_repository.rb +49 -0
- data/lib/appium_failure_helper/handler.rb +55 -0
- data/lib/appium_failure_helper/page_analyzer.rb +109 -0
- data/lib/appium_failure_helper/report_generator.rb +160 -0
- data/lib/appium_failure_helper/utils.rb +17 -0
- data/lib/appium_failure_helper/version.rb +1 -1
- data/lib/appium_failure_helper.rb +18 -3
- metadata +7 -2
- data/lib/appium_failure_helper/capture.rb +0 -346
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc097199adcadcecb8d7492ab6e2c6f577b0cdf9acfa28cb9486195b9342c1f6
|
4
|
+
data.tar.gz: 5e92c1e6cba8685d9254ff550c1eee4a553afaffc1c4b1e726f410f9ed3fa7e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7fd06c93b630f5f70163f2b3396fb0b078d902393f73b7729f94564dbc17f3e90c0aaf42da166728b183a306ee4d12671226a8ac8a45de75f1981d74f04905f8
|
7
|
+
data.tar.gz: 8629aa0822ef81b2ea24c8048ee31445d92e195a5404d830b7db7aae19c2606e261fc941aa9d5ca920087ad975af342de489bdc32f355226bfa66d4c97772ec9
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module AppiumFailureHelper
|
2
|
+
module Analyzer
|
3
|
+
def self.extract_failure_details(exception)
|
4
|
+
message = exception.message
|
5
|
+
info = {}
|
6
|
+
patterns = [
|
7
|
+
/element with locator ['"]?(#?\w+)['"]?/i,
|
8
|
+
/(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
|
9
|
+
/no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
|
10
|
+
/(?:with the resource-id|with the accessibility-id) ['"]?(.+?)['"]?/i
|
11
|
+
]
|
12
|
+
patterns.each do |pattern|
|
13
|
+
match = message.match(pattern)
|
14
|
+
if match
|
15
|
+
info[:selector_value] = match.captures.last.strip.gsub(/['"]/, '')
|
16
|
+
info[:selector_type] = match.captures.size > 1 ? match.captures[0].strip.gsub(/['"]/, '') : 'id'
|
17
|
+
return info
|
18
|
+
end
|
19
|
+
end
|
20
|
+
info
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.find_de_para_match(failed_info, element_map)
|
24
|
+
logical_name_key = failed_info[:selector_value].to_s.gsub(/^#/, '')
|
25
|
+
if element_map.key?(logical_name_key)
|
26
|
+
return {
|
27
|
+
logical_name: logical_name_key,
|
28
|
+
correct_locator: element_map[logical_name_key]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.find_similar_elements(failed_info, all_page_suggestions)
|
35
|
+
failed_locator_value = failed_info[:selector_value]
|
36
|
+
failed_locator_type = failed_info[:selector_type]
|
37
|
+
return [] unless failed_locator_value && failed_locator_type
|
38
|
+
|
39
|
+
normalized_failed_type = failed_locator_type.downcase.include?('id') ? 'id' : failed_locator_type
|
40
|
+
|
41
|
+
cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
42
|
+
similarities = []
|
43
|
+
|
44
|
+
all_page_suggestions.each do |suggestion|
|
45
|
+
candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
|
46
|
+
next unless candidate_locator
|
47
|
+
|
48
|
+
cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
49
|
+
distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
|
50
|
+
max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
|
51
|
+
next if max_len.zero?
|
52
|
+
|
53
|
+
similarity_score = 1.0 - (distance.to_f / max_len)
|
54
|
+
if similarity_score > 0.8
|
55
|
+
similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
similarities.sort_by { |s| -s[:score] }.first(5)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module AppiumFailureHelper
|
2
|
+
module ElementRepository
|
3
|
+
def self.load_all_from_yaml
|
4
|
+
elements_map = {}
|
5
|
+
# Procura em todo o diretório de trabalho atual por arquivos .yaml
|
6
|
+
glob_path = File.join(Dir.pwd, '**', '*.yaml')
|
7
|
+
|
8
|
+
Dir.glob(glob_path).each do |file|
|
9
|
+
# Evita ler os próprios relatórios gerados
|
10
|
+
next if file.include?('reports_failure')
|
11
|
+
|
12
|
+
begin
|
13
|
+
data = YAML.load_file(file)
|
14
|
+
if data.is_a?(Hash)
|
15
|
+
data.each do |key, value|
|
16
|
+
if value.is_a?(Hash) && value['tipoBusca'] && value['value']
|
17
|
+
elements_map[key] = value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
rescue => e
|
22
|
+
Utils.logger.warn("Aviso: Erro ao carregar o arquivo YAML #{file}: #{e.message}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
elements_map
|
26
|
+
end
|
27
|
+
|
28
|
+
# NOVO: Método para verificar a existência de um elemento em um arquivo .rb
|
29
|
+
def self.find_in_ruby_file(element_name, path = 'elements/elements.rb')
|
30
|
+
return { found: false, path: path, reason: "Arquivo não encontrado" } unless File.exist?(path)
|
31
|
+
|
32
|
+
begin
|
33
|
+
content = File.read(path)
|
34
|
+
# Regex flexível para encontrar definições como:
|
35
|
+
# def nome_do_elemento
|
36
|
+
# element :nome_do_elemento
|
37
|
+
# element('nome_do_elemento')
|
38
|
+
if content.match?(/def #{element_name}|element[ |\(]['|:]#{element_name}/)
|
39
|
+
return { found: true, path: path }
|
40
|
+
else
|
41
|
+
return { found: false, path: path, reason: "Definição não encontrada" }
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
Utils.logger.warn("Aviso: Erro ao ler o arquivo Ruby #{path}: #{e.message}")
|
45
|
+
return { found: false, path: path, reason: "Erro de leitura" }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module AppiumFailureHelper
|
2
|
+
class Handler
|
3
|
+
def self.call(driver, exception)
|
4
|
+
new(driver, exception).call
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(driver, exception)
|
8
|
+
@driver = driver
|
9
|
+
@exception = exception
|
10
|
+
@timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
11
|
+
@output_folder = "reports_failure/failure_#{@timestamp}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
FileUtils.mkdir_p(@output_folder)
|
16
|
+
page_source = @driver.page_source
|
17
|
+
platform = @driver.capabilities['platformName']&.downcase || 'unknown'
|
18
|
+
|
19
|
+
failed_info = Analyzer.extract_failure_details(@exception)
|
20
|
+
|
21
|
+
# ALTERADO: Agora busca em todas as fontes de dados
|
22
|
+
logical_name_key = failed_info[:selector_value].to_s.gsub(/^#/, '')
|
23
|
+
|
24
|
+
# 1. Busca nos arquivos YAML dinâmicos
|
25
|
+
element_map_yaml = ElementRepository.load_all_from_yaml
|
26
|
+
de_para_yaml_result = Analyzer.find_de_para_match(failed_info, element_map_yaml)
|
27
|
+
|
28
|
+
# 2. Busca no arquivo Ruby
|
29
|
+
de_para_rb_result = ElementRepository.find_in_ruby_file(logical_name_key)
|
30
|
+
|
31
|
+
# 3. Analisa a tela atual
|
32
|
+
page_analyzer = PageAnalyzer.new(page_source, platform)
|
33
|
+
all_page_elements = page_analyzer.analyze
|
34
|
+
similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements)
|
35
|
+
|
36
|
+
# Organiza TODOS os resultados para o relatório
|
37
|
+
report_data = {
|
38
|
+
failed_element: failed_info,
|
39
|
+
similar_elements: similar_elements,
|
40
|
+
de_para_yaml_analysis: de_para_yaml_result, # Resultado da análise YAML
|
41
|
+
de_para_rb_analysis: de_para_rb_result, # Resultado da análise Ruby
|
42
|
+
all_page_elements: all_page_elements,
|
43
|
+
screenshot_base64: @driver.screenshot_as(:base64),
|
44
|
+
platform: platform,
|
45
|
+
timestamp: @timestamp
|
46
|
+
}
|
47
|
+
|
48
|
+
ReportGenerator.new(@output_folder, page_source, report_data).generate_all
|
49
|
+
|
50
|
+
Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
|
51
|
+
rescue => e
|
52
|
+
Utils.logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# lib/appium_failure_helper/page_analyzer.rb
|
2
|
+
module AppiumFailureHelper
|
3
|
+
class PageAnalyzer
|
4
|
+
PREFIX = {
|
5
|
+
'android.widget.Button' => 'btn', 'android.widget.TextView' => 'txt',
|
6
|
+
'android.widget.ImageView' => 'img', 'android.widget.EditText' => 'input',
|
7
|
+
'android.widget.CheckBox' => 'chk', 'android.widget.RadioButton' => 'radio',
|
8
|
+
'android.widget.Switch' => 'switch', 'android.widget.ViewGroup' => 'group',
|
9
|
+
'android.widget.View' => 'view', 'android.widget.FrameLayout' => 'frame',
|
10
|
+
'android.widget.LinearLayout' => 'linear', 'android.widget.RelativeLayout' => 'relative',
|
11
|
+
'android.widget.ScrollView' => 'scroll', 'android.webkit.WebView' => 'web',
|
12
|
+
'android.widget.Spinner' => 'spin', 'XCUIElementTypeButton' => 'btn',
|
13
|
+
'XCUIElementTypeStaticText' => 'txt', 'XCUIElementTypeTextField' => 'input',
|
14
|
+
'XCUIElementTypeImage' => 'img', 'XCUIElementTypeSwitch' => 'switch',
|
15
|
+
'XCUIElementTypeScrollView' => 'scroll', 'XCUIElementTypeOther' => 'elm',
|
16
|
+
'XCUIElementTypeCell' => 'cell'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
def initialize(page_source, platform)
|
20
|
+
@doc = Nokogiri::XML(page_source)
|
21
|
+
@platform = platform
|
22
|
+
end
|
23
|
+
|
24
|
+
def analyze
|
25
|
+
seen_elements = {}
|
26
|
+
all_elements_suggestions = []
|
27
|
+
@doc.xpath('//*').each do |node|
|
28
|
+
next if ['hierarchy', 'AppiumAUT'].include?(node.name)
|
29
|
+
attrs = node.attributes.transform_values(&:value)
|
30
|
+
unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
|
31
|
+
unless seen_elements[unique_key]
|
32
|
+
name = suggest_name(node.name, attrs)
|
33
|
+
locators = xpath_generator(node.name, attrs)
|
34
|
+
all_elements_suggestions << { name: name, locators: locators }
|
35
|
+
seen_elements[unique_key] = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
all_elements_suggestions
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def suggest_name(tag, attrs)
|
44
|
+
type = tag.split('.').last
|
45
|
+
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
46
|
+
name_base = nil
|
47
|
+
|
48
|
+
priority_attrs = if tag.start_with?('XCUIElementType')
|
49
|
+
['name', 'label', 'value']
|
50
|
+
else
|
51
|
+
['content-desc', 'text', 'resource-id']
|
52
|
+
end
|
53
|
+
|
54
|
+
priority_attrs.each do |attr_key|
|
55
|
+
value = attrs[attr_key]
|
56
|
+
if value.is_a?(String) && !value.empty?
|
57
|
+
name_base = value
|
58
|
+
break
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
name_base ||= type.gsub('XCUIElementType', '')
|
63
|
+
|
64
|
+
truncated_name = Utils.truncate(name_base)
|
65
|
+
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
66
|
+
|
67
|
+
"#{pfx}#{sanitized_name}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def xpath_generator(tag, attrs)
|
71
|
+
case @platform
|
72
|
+
when 'android' then generate_android_xpaths(tag, attrs)
|
73
|
+
when 'ios' then generate_ios_xpaths(tag, attrs)
|
74
|
+
else generate_unknown_xpaths(tag, attrs)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_android_xpaths(tag, attrs)
|
79
|
+
locators = []
|
80
|
+
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
81
|
+
locators << { strategy: 'id', locator: attrs['resource-id'] }
|
82
|
+
end
|
83
|
+
if attrs['text'] && !attrs['text'].empty?
|
84
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{Utils.truncate(attrs['text'])}\"]" }
|
85
|
+
end
|
86
|
+
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
87
|
+
locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{Utils.truncate(attrs['content-desc'])}\"]" }
|
88
|
+
end
|
89
|
+
locators
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_ios_xpaths(tag, attrs)
|
93
|
+
locators = []
|
94
|
+
if attrs['name'] && !attrs['name'].empty?
|
95
|
+
locators << { strategy: 'name', locator: attrs['name'] }
|
96
|
+
end
|
97
|
+
if attrs['label'] && !attrs['label'].empty?
|
98
|
+
locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{Utils.truncate(attrs['label'])}\"]" }
|
99
|
+
end
|
100
|
+
locators
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_unknown_xpaths(tag, attrs)
|
104
|
+
locators = []
|
105
|
+
attrs.each { |key, value| locators << { strategy: key.to_s, locator: "//#{tag}[@#{key}=\"#{Utils.truncate(value)}\"]" } if value.is_a?(String) && !value.empty? }
|
106
|
+
locators
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module AppiumFailureHelper
|
2
|
+
class ReportGenerator
|
3
|
+
def initialize(output_folder, page_source, report_data)
|
4
|
+
@output_folder = output_folder
|
5
|
+
@page_source = page_source
|
6
|
+
@data = report_data
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate_all
|
10
|
+
generate_xml_report
|
11
|
+
generate_yaml_reports
|
12
|
+
generate_html_report
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def generate_xml_report
|
18
|
+
File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_yaml_reports
|
22
|
+
analysis_report = {
|
23
|
+
failed_element: @data[:failed_element],
|
24
|
+
similar_elements: @data[:similar_elements],
|
25
|
+
de_para_yaml_analysis: @data[:de_para_yaml_analysis],
|
26
|
+
de_para_rb_analysis: @data[:de_para_rb_analysis]
|
27
|
+
}
|
28
|
+
File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
|
29
|
+
File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_html_report
|
33
|
+
# ... (Lambdas locators_html e all_elements_html permanecem iguais) ...
|
34
|
+
locators_html = ->(locators) { locators.map { |loc| "..." }.join }
|
35
|
+
all_elements_html = ->(elements) { elements.map { |el| "..." }.join }
|
36
|
+
|
37
|
+
# Prepara o conteúdo dinâmico
|
38
|
+
failed_info = @data[:failed_element]
|
39
|
+
similar_elements = @data[:similar_elements]
|
40
|
+
de_para_yaml = @data[:de_para_yaml_analysis]
|
41
|
+
de_para_rb = @data[:de_para_rb_analysis]
|
42
|
+
all_suggestions = @data[:all_page_elements]
|
43
|
+
|
44
|
+
# NOVO: Gera dois blocos de HTML, um para cada análise
|
45
|
+
|
46
|
+
# Bloco de análise YAML
|
47
|
+
de_para_yaml_html = ""
|
48
|
+
if de_para_yaml
|
49
|
+
de_para_yaml_html = <<~HTML
|
50
|
+
<div class="bg-green-50 border border-green-200 p-4 rounded-lg shadow-md mb-6">
|
51
|
+
<h3 class="text-lg font-bold text-green-800 mb-2">Análise de Mapeamento YAML (.yaml)</h3>
|
52
|
+
<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_yaml[:logical_name])}</strong> foi encontrado nos arquivos .yaml do projeto!</p>
|
53
|
+
<p class="text-sm text-gray-700">O localizador correto definido é:</p>
|
54
|
+
<div class="font-mono text-xs bg-green-100 p-2 mt-2 rounded">
|
55
|
+
<span class="font-bold">#{CGI.escapeHTML(de_para_yaml[:correct_locator]['tipoBusca'].upcase)}:</span>
|
56
|
+
<span class="break-all">#{CGI.escapeHTML(de_para_yaml[:correct_locator]['valor'])}</span>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
HTML
|
60
|
+
end
|
61
|
+
|
62
|
+
# Bloco de análise Ruby
|
63
|
+
de_para_rb_html = ""
|
64
|
+
if de_para_rb
|
65
|
+
if de_para_rb[:found]
|
66
|
+
de_para_rb_html = <<~HTML
|
67
|
+
<div class="bg-green-50 border border-green-200 p-4 rounded-lg shadow-md mb-6">
|
68
|
+
<h3 class="text-lg font-bold text-green-800 mb-2">Análise de Mapeamento Ruby (.rb)</h3>
|
69
|
+
<p class="text-sm text-gray-700">A definição do elemento foi encontrada com sucesso no arquivo <strong class="font-mono bg-gray-200 px-1 rounded">#{de_para_rb[:path]}</strong>.</p>
|
70
|
+
</div>
|
71
|
+
HTML
|
72
|
+
else
|
73
|
+
de_para_rb_html = <<~HTML
|
74
|
+
<div class="bg-yellow-50 border border-yellow-200 p-4 rounded-lg shadow-md mb-6">
|
75
|
+
<h3 class="text-lg font-bold text-yellow-800 mb-2">Análise de Mapeamento Ruby (.rb)</h3>
|
76
|
+
<p class="text-sm text-gray-700">O elemento <strong class="font-mono bg-gray-200 px-1 rounded">#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</strong> NÃO foi encontrado no arquivo <strong class="font-mono bg-gray-200 px-1 rounded">#{de_para_rb[:path]}</strong>.</p>
|
77
|
+
<p class="text-xs text-gray-500 mt-1">Motivo: #{de_para_rb[:reason]}</p>
|
78
|
+
</div>
|
79
|
+
HTML
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>" : similar_elements.map { |el|
|
84
|
+
score_percent = (el[:score] * 100).round(1)
|
85
|
+
"<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>"
|
86
|
+
}.join
|
87
|
+
|
88
|
+
failed_info_content = if failed_info && failed_info[:selector_value]
|
89
|
+
"<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>"
|
90
|
+
else
|
91
|
+
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
|
92
|
+
end
|
93
|
+
|
94
|
+
html_content = <<~HTML_REPORT
|
95
|
+
<!DOCTYPE html>
|
96
|
+
<html lang="pt-BR">
|
97
|
+
<head>
|
98
|
+
<meta charset="UTF-8">
|
99
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
100
|
+
<title>Relatório de Falha Appium - #{@data[:timestamp]}</title>
|
101
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
102
|
+
<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>
|
103
|
+
</head>
|
104
|
+
<body class="bg-gray-50 p-8">
|
105
|
+
<div class="max-w-7xl mx-auto">
|
106
|
+
<header class="mb-8 pb-4 border-b border-gray-300">
|
107
|
+
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
108
|
+
<p class="text-sm text-gray-500">Relatório gerado em: #{@data[:timestamp]} | Plataforma: #{@data[:platform].upcase}</p>
|
109
|
+
</header>
|
110
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
111
|
+
<div class="lg:col-span-1">
|
112
|
+
#{de_para_rb_html}
|
113
|
+
<div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
|
114
|
+
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
115
|
+
#{failed_info_content}
|
116
|
+
</div>
|
117
|
+
<div class="bg-white p-4 rounded-lg shadow-xl">
|
118
|
+
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
119
|
+
<img src="data:image/png;base64,#{@data[:screenshot_base64]}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
120
|
+
</div>
|
121
|
+
</div>
|
122
|
+
<div class="lg:col-span-2">
|
123
|
+
<div class="bg-white rounded-lg shadow-xl">
|
124
|
+
<div class="flex border-b border-gray-200">
|
125
|
+
<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>
|
126
|
+
<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>
|
127
|
+
</div>
|
128
|
+
<div class="p-6">
|
129
|
+
<div id="similar" class="tab-content active">
|
130
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
|
131
|
+
<div class="space-y-4">#{similar_elements_content}</div>
|
132
|
+
</div>
|
133
|
+
<div id="all" class="tab-content">
|
134
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump de Todos os Elementos da Tela</h3>
|
135
|
+
<div class="max-h-[600px] overflow-y-auto space-y-2">#{all_elements_html.call(all_suggestions)}</div>
|
136
|
+
</div>
|
137
|
+
</div>
|
138
|
+
</div>
|
139
|
+
</div>
|
140
|
+
</div>
|
141
|
+
</div>
|
142
|
+
<script>
|
143
|
+
document.querySelectorAll('.tab-button').forEach(tab => {
|
144
|
+
tab.addEventListener('click', () => {
|
145
|
+
const target = tab.getAttribute('data-tab');
|
146
|
+
document.querySelectorAll('.tab-button').forEach(t => t.classList.remove('active'));
|
147
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
148
|
+
tab.classList.add('active');
|
149
|
+
document.getElementById(target).classList.add('active');
|
150
|
+
});
|
151
|
+
});
|
152
|
+
</script>
|
153
|
+
</body>
|
154
|
+
</html>
|
155
|
+
HTML_REPORT
|
156
|
+
|
157
|
+
File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AppiumFailureHelper
|
2
|
+
module Utils
|
3
|
+
def self.logger
|
4
|
+
@logger ||= begin
|
5
|
+
logger = Logger.new(STDOUT)
|
6
|
+
logger.level = Logger::INFO
|
7
|
+
logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n" }
|
8
|
+
logger
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.truncate(value, max_length = 100)
|
13
|
+
return value unless value.is_a?(String)
|
14
|
+
value.size > max_length ? "#{value[0...max_length]}..." : value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,8 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'base64'
|
6
|
+
require 'yaml'
|
7
|
+
require 'logger'
|
8
|
+
require 'did_you_mean'
|
9
|
+
require 'cgi'
|
10
|
+
|
11
|
+
# Carrega todos os nossos novos módulos
|
12
|
+
require_relative 'appium_failure_helper/utils'
|
13
|
+
require_relative 'appium_failure_helper/analyzer'
|
14
|
+
require_relative 'appium_failure_helper/element_repository'
|
15
|
+
require_relative 'appium_failure_helper/page_analyzer'
|
16
|
+
require_relative 'appium_failure_helper/report_generator'
|
17
|
+
require_relative 'appium_failure_helper/handler'
|
5
18
|
module AppiumFailureHelper
|
6
19
|
class Error < StandardError; end
|
7
|
-
|
20
|
+
def self.handler_failure(driver, exception)
|
21
|
+
Handler.call(driver, exception)
|
22
|
+
end
|
8
23
|
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: 0.6.
|
4
|
+
version: 0.6.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Nascimento
|
@@ -94,7 +94,12 @@ files:
|
|
94
94
|
- README.md
|
95
95
|
- Rakefile
|
96
96
|
- lib/appium_failure_helper.rb
|
97
|
-
- lib/appium_failure_helper/
|
97
|
+
- lib/appium_failure_helper/analyzer.rb
|
98
|
+
- lib/appium_failure_helper/element_repository.rb
|
99
|
+
- lib/appium_failure_helper/handler.rb
|
100
|
+
- lib/appium_failure_helper/page_analyzer.rb
|
101
|
+
- lib/appium_failure_helper/report_generator.rb
|
102
|
+
- lib/appium_failure_helper/utils.rb
|
98
103
|
- lib/appium_failure_helper/version.rb
|
99
104
|
- sig/appium_failure_helper.rbs
|
100
105
|
homepage: https://github.com/David-Nascimento/Appium_failure_helper
|
@@ -1,346 +0,0 @@
|
|
1
|
-
require 'nokogiri'
|
2
|
-
require 'fileutils'
|
3
|
-
require 'base64'
|
4
|
-
require 'yaml'
|
5
|
-
require 'logger'
|
6
|
-
require 'did_you_mean'
|
7
|
-
require 'cgi' # Adicionado para garantir o escape de HTML
|
8
|
-
|
9
|
-
module AppiumFailureHelper
|
10
|
-
class Capture
|
11
|
-
# ... (Constantes PREFIX, MAX_VALUE_LENGTH, @@logger permanecem iguais) ...
|
12
|
-
PREFIX = {
|
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'
|
25
|
-
}.freeze
|
26
|
-
MAX_VALUE_LENGTH = 100
|
27
|
-
@@logger = nil
|
28
|
-
|
29
|
-
# --- MÉTODO PRINCIPAL (SEM ALTERAÇÕES) ---
|
30
|
-
def self.handler_failure(driver, exception)
|
31
|
-
begin
|
32
|
-
self.setup_logger unless @@logger
|
33
|
-
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
34
|
-
output_folder = "reports_failure/failure_#{timestamp}"
|
35
|
-
FileUtils.mkdir_p(output_folder)
|
36
|
-
@@logger.info("Pasta de saída criada: #{output_folder}")
|
37
|
-
screenshot_base64 = driver.screenshot_as(:base64)
|
38
|
-
page_source = driver.page_source
|
39
|
-
File.write("#{output_folder}/page_source_#{timestamp}.xml", page_source)
|
40
|
-
doc = Nokogiri::XML(page_source)
|
41
|
-
platform = driver.capabilities['platformName']&.downcase || 'unknown'
|
42
|
-
failed_element_info = self.extract_info_from_exception(exception)
|
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
|
-
}
|
51
|
-
end
|
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)
|
54
|
-
targeted_report = {
|
55
|
-
failed_element: failed_element_info,
|
56
|
-
similar_elements: similar_elements,
|
57
|
-
de_para_analysis: de_para_result
|
58
|
-
}
|
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)) }
|
61
|
-
html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
|
62
|
-
File.write("#{output_folder}/report_#{timestamp}.html", html_content)
|
63
|
-
@@logger.info("Relatórios gerados com sucesso em: #{output_folder}")
|
64
|
-
rescue => e
|
65
|
-
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def self.load_local_element_map
|
72
|
-
elements_map = {}
|
73
|
-
glob_path = File.join(Dir.pwd, 'features', 'elements', '**', '*.yaml')
|
74
|
-
Dir.glob(glob_path).each do |file|
|
75
|
-
begin
|
76
|
-
data = YAML.load_file(file)
|
77
|
-
if data.is_a?(Hash)
|
78
|
-
data.each do |key, value|
|
79
|
-
if value.is_a?(Hash) && value['tipoBusca'] && value['valor']
|
80
|
-
elements_map[key] = value
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
rescue => e
|
85
|
-
@@logger.warn("Aviso: Erro ao carregar o arquivo de elementos #{file}: #{e.message}") if @@logger
|
86
|
-
end
|
87
|
-
end
|
88
|
-
elements_map
|
89
|
-
end
|
90
|
-
|
91
|
-
def self.setup_logger
|
92
|
-
@@logger = Logger.new(STDOUT)
|
93
|
-
@@logger.level = Logger::INFO
|
94
|
-
@@logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n" }
|
95
|
-
end
|
96
|
-
|
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
|
133
|
-
|
134
|
-
cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
135
|
-
distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
|
136
|
-
max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
|
137
|
-
next if max_len.zero?
|
138
|
-
|
139
|
-
similarity_score = 1.0 - (distance.to_f / max_len)
|
140
|
-
if similarity_score > 0.8 # Aumenta o limiar para maior precisão
|
141
|
-
similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score }
|
142
|
-
end
|
143
|
-
end
|
144
|
-
similarities.sort_by { |s| -s[:score] }.first(5)
|
145
|
-
end
|
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
|
-
|
165
|
-
def self.truncate(value)
|
166
|
-
return value unless value.is_a?(String)
|
167
|
-
value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
|
168
|
-
end
|
169
|
-
|
170
|
-
def self.suggest_name(tag, attrs)
|
171
|
-
type = tag.split('.').last
|
172
|
-
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
173
|
-
name_base = nil
|
174
|
-
|
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|
|
182
|
-
value = attrs[attr_key]
|
183
|
-
if value.is_a?(String) && !value.empty?
|
184
|
-
name_base = value
|
185
|
-
break
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
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
|
191
|
-
|
192
|
-
truncated_name = truncate(name_base)
|
193
|
-
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
194
|
-
|
195
|
-
"#{pfx}#{sanitized_name}"
|
196
|
-
end
|
197
|
-
|
198
|
-
def self.xpath_generator(tag, attrs, platform)
|
199
|
-
case platform
|
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)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
def self.generate_android_xpaths(tag, attrs)
|
207
|
-
locators = []
|
208
|
-
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
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'] }
|
211
|
-
end
|
212
|
-
if attrs['text'] && !attrs['text'].empty?
|
213
|
-
locators << { strategy: 'xpath', locator: "//#{tag}[@text=\"#{truncate(attrs['text'])}\"]" }
|
214
|
-
end
|
215
|
-
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
216
|
-
locators << { strategy: 'xpath_desc', locator: "//#{tag}[@content-desc=\"#{truncate(attrs['content-desc'])}\"]" }
|
217
|
-
end
|
218
|
-
locators
|
219
|
-
end
|
220
|
-
|
221
|
-
def self.generate_ios_xpaths(tag, attrs)
|
222
|
-
locators = []
|
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'] }
|
226
|
-
end
|
227
|
-
if attrs['label'] && !attrs['label'].empty?
|
228
|
-
locators << { strategy: 'xpath', locator: "//#{tag}[@label=\"#{truncate(attrs['label'])}\"]" }
|
229
|
-
end
|
230
|
-
locators
|
231
|
-
end
|
232
|
-
|
233
|
-
def self.generate_unknown_xpaths(tag, attrs)
|
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
|
237
|
-
end
|
238
|
-
|
239
|
-
def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
|
240
|
-
# Lambdas para gerar partes do HTML
|
241
|
-
locators_html = lambda do |locators|
|
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
|
243
|
-
end
|
244
|
-
|
245
|
-
all_elements_html = lambda do |elements|
|
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
|
247
|
-
end
|
248
|
-
|
249
|
-
# Prepara o conteúdo dinâmico
|
250
|
-
failed_info = targeted_report[:failed_element]
|
251
|
-
similar_elements = targeted_report[:similar_elements]
|
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
|
-
|
275
|
-
failed_info_content = if failed_info && failed_info[:selector_value]
|
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>"
|
277
|
-
else
|
278
|
-
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
|
279
|
-
end
|
280
|
-
|
281
|
-
# Template HTML completo
|
282
|
-
<<~HTML_REPORT
|
283
|
-
<!DOCTYPE html>
|
284
|
-
<html lang="pt-BR">
|
285
|
-
<head>
|
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>
|
291
|
-
</head>
|
292
|
-
<body class="bg-gray-50 p-8">
|
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>
|
320
|
-
</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>
|
324
|
-
</div>
|
325
|
-
</div>
|
326
|
-
</div>
|
327
|
-
</div>
|
328
|
-
</div>
|
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>
|
341
|
-
</body>
|
342
|
-
</html>
|
343
|
-
HTML_REPORT
|
344
|
-
end
|
345
|
-
end
|
346
|
-
end
|