appium_failure_helper 0.6.1 → 0.6.3
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 -466
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8dfc8ecc63d0e6f70e9ae388b3f5be698586248af80feec88a531e6333273d82
|
4
|
+
data.tar.gz: c2a98c1a05885d7ea36b13a3e3366c7b848e76a055dd95f831f6272d6832a880
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb9efe4166951dff46f7f8fbed2753ab825eded0216172f9cb62c385cf74bc63c44aa1f1ea3288a7dccd511d29a712733ec96aebeb01095c687227cfd6e5726d
|
7
|
+
data.tar.gz: eb2f4561139100b968b778545cf12ce537e4a3e28468e731214f6f24a748dd86158afc0c8b8d81221eb0a5c03459dc3aa988747430bd8cac24ac9fa723755742
|
@@ -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_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.3
|
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,466 +0,0 @@
|
|
1
|
-
require 'nokogiri'
|
2
|
-
require 'fileutils'
|
3
|
-
require 'base64'
|
4
|
-
require 'yaml'
|
5
|
-
require 'logger'
|
6
|
-
|
7
|
-
module AppiumFailureHelper
|
8
|
-
class Capture
|
9
|
-
PREFIX = {
|
10
|
-
'android.widget.Button' => 'btn',
|
11
|
-
'android.widget.TextView' => 'txt',
|
12
|
-
'android.widget.ImageView' => 'img',
|
13
|
-
'android.widget.EditText' => 'input',
|
14
|
-
'android.widget.CheckBox' => 'chk',
|
15
|
-
'android.widget.RadioButton' => 'radio',
|
16
|
-
'android.widget.Switch' => 'switch',
|
17
|
-
'android.widget.ViewGroup' => 'group',
|
18
|
-
'android.widget.View' => 'view',
|
19
|
-
'android.widget.FrameLayout' => 'frame',
|
20
|
-
'android.widget.LinearLayout' => 'linear',
|
21
|
-
'android.widget.RelativeLayout' => 'relative',
|
22
|
-
'android.widget.ScrollView' => 'scroll',
|
23
|
-
'android.webkit.WebView' => 'web',
|
24
|
-
'android.widget.Spinner' => 'spin',
|
25
|
-
'XCUIElementTypeButton' => 'btn',
|
26
|
-
'XCUIElementTypeStaticText' => 'txt',
|
27
|
-
'XCUIElementTypeTextField' => 'input',
|
28
|
-
'XCUIElementTypeImage' => 'img',
|
29
|
-
'XCUIElementTypeSwitch' => 'switch',
|
30
|
-
'XCUIElementTypeScrollView' => 'scroll',
|
31
|
-
'XCUIElementTypeOther' => 'elm',
|
32
|
-
'XCUIElementTypeCell' => 'cell',
|
33
|
-
}.freeze
|
34
|
-
|
35
|
-
MAX_VALUE_LENGTH = 100
|
36
|
-
@@logger = nil
|
37
|
-
|
38
|
-
def self.handler_failure(driver, exception)
|
39
|
-
begin
|
40
|
-
self.setup_logger unless @@logger
|
41
|
-
|
42
|
-
FileUtils.rm_rf("reports_failure")
|
43
|
-
@@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
|
44
|
-
|
45
|
-
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
46
|
-
output_folder = "reports_failure/failure_#{timestamp}"
|
47
|
-
|
48
|
-
FileUtils.mkdir_p(output_folder)
|
49
|
-
@@logger.info("Pasta de saída criada: #{output_folder}")
|
50
|
-
|
51
|
-
screenshot_base64 = driver.screenshot_as(:base64)
|
52
|
-
screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
|
53
|
-
File.open(screenshot_path, 'wb') do |f|
|
54
|
-
f.write(Base64.decode64(screenshot_base64))
|
55
|
-
end
|
56
|
-
@@logger.info("Screenshot salvo em #{screenshot_path}")
|
57
|
-
|
58
|
-
page_source = driver.page_source
|
59
|
-
xml_path = "#{output_folder}/page_source_#{timestamp}.xml"
|
60
|
-
File.write(xml_path, page_source)
|
61
|
-
@@logger.info("Page source salvo em #{xml_path}")
|
62
|
-
|
63
|
-
doc = Nokogiri::XML(page_source)
|
64
|
-
platform = driver.capabilities['platformName']&.downcase || 'unknown'
|
65
|
-
|
66
|
-
failed_element_info = self.extract_info_from_exception(exception)
|
67
|
-
|
68
|
-
seen_elements = {}
|
69
|
-
all_elements_suggestions = []
|
70
|
-
doc.xpath('//*').each do |node|
|
71
|
-
next if node.name == 'hierarchy'
|
72
|
-
attrs = node.attributes.transform_values(&:value)
|
73
|
-
|
74
|
-
unique_key = "#{node.name}|#{attrs['resource-id'].to_s}|#{attrs['content-desc'].to_s}|#{attrs['text'].to_s}"
|
75
|
-
|
76
|
-
unless seen_elements[unique_key]
|
77
|
-
name = self.suggest_name(node.name, attrs)
|
78
|
-
locators = self.xpath_generator(node.name, attrs, platform)
|
79
|
-
|
80
|
-
all_elements_suggestions << { name: name, locators: locators }
|
81
|
-
seen_elements[unique_key] = true
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
targeted_report = {
|
86
|
-
failed_element: failed_element_info,
|
87
|
-
similar_elements: [],
|
88
|
-
}
|
89
|
-
|
90
|
-
if failed_element_info && failed_element_info[:selector_value]
|
91
|
-
targeted_report[:similar_elements] = self.find_similar_elements(doc, failed_element_info, platform)
|
92
|
-
end
|
93
|
-
|
94
|
-
targeted_yaml_path = "#{output_folder}/failure_analysis_#{timestamp}.yaml"
|
95
|
-
File.open(targeted_yaml_path, 'w') do |f|
|
96
|
-
f.write(YAML.dump(targeted_report))
|
97
|
-
end
|
98
|
-
@@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
|
99
|
-
|
100
|
-
full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
|
101
|
-
File.open(full_dump_yaml_path, 'w') do |f|
|
102
|
-
f.write(YAML.dump(all_elements_suggestions))
|
103
|
-
end
|
104
|
-
@@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
|
105
|
-
|
106
|
-
html_report_path = "#{output_folder}/report_#{timestamp}.html"
|
107
|
-
html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
|
108
|
-
File.write(html_report_path, html_content)
|
109
|
-
@@logger.info("Relatório HTML completo salvo em #{html_report_path}")
|
110
|
-
|
111
|
-
rescue => e
|
112
|
-
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
private
|
117
|
-
|
118
|
-
def self.setup_logger
|
119
|
-
@@logger ||= Logger.new(STDOUT)
|
120
|
-
@@logger.level = Logger::INFO
|
121
|
-
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
122
|
-
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def self.extract_info_from_exception(exception)
|
127
|
-
message = exception.to_s
|
128
|
-
info = {}
|
129
|
-
|
130
|
-
# normaliza tipos capturados para nomes previsíveis
|
131
|
-
normalize_type = lambda do |t|
|
132
|
-
return 'unknown' unless t
|
133
|
-
t = t.to_s.downcase.strip
|
134
|
-
t = t.gsub(/["']/, '')
|
135
|
-
case t
|
136
|
-
when 'cssselector', 'css selector' then 'css'
|
137
|
-
when 'classname', 'class name' then 'class name'
|
138
|
-
when 'accessibilityid', 'accessibility-id', 'accessibility id' then 'accessibility-id'
|
139
|
-
when 'resourceid', 'resource-id', 'resource id', 'id' then 'resource-id'
|
140
|
-
when 'contentdesc', 'content-desc', 'content desc' then 'content-desc'
|
141
|
-
else
|
142
|
-
t.gsub(/\s+/, '_').gsub(/[^a-z0-9_\-]/, '')
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
patterns = [
|
147
|
-
# ChromeDriver/Selenium JSON style:
|
148
|
-
# no such element: Unable to locate element: {"method":"xpath","selector":"//..."}
|
149
|
-
/no such element: Unable to locate element:\s*\{\s*["']?method["']?\s*:\s*["']?([^"'\}]+)["']?\s*,\s*["']?selector["']?\s*:\s*["']?([^"']+)["']?\s*\}/i,
|
150
|
-
|
151
|
-
# By.xpath: //..., By.id: "foo"
|
152
|
-
/By\.(xpath|id|css selector|cssSelector|name|class name|className):\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
153
|
-
|
154
|
-
# Generic "using <type>=<value>" or using <type>: '<value>'
|
155
|
-
/using\s+([a-zA-Z0-9_\-:]+)\s*[=:]\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
156
|
-
|
157
|
-
# "An element with the selector '...' was not found"
|
158
|
-
/An element with (?:the )?selector ['"](.+?)['"] (?:could not be found|was not found|not found|not be located)/i,
|
159
|
-
|
160
|
-
# "with the resource-id 'xyz'" or "with the accessibility-id 'abc'"
|
161
|
-
/with the (resource-id|accessibility[- ]?id|content-?desc|label|name)\s*[:=]?\s*['"](.+?)['"]/i,
|
162
|
-
|
163
|
-
# "Unable to find element by: id 'xyz'"
|
164
|
-
/Unable to find element by:\s*([a-zA-Z0-9_\- ]+)\s*[:=]?\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
165
|
-
|
166
|
-
# Fallback to any "selector: '...'" occurence
|
167
|
-
/selector['"]?\s*[:=]?\s*['"](.+?)['"]/i
|
168
|
-
]
|
169
|
-
|
170
|
-
patterns.each do |pattern|
|
171
|
-
if (m = message.match(pattern))
|
172
|
-
caps = m.captures.compact
|
173
|
-
if caps.length >= 2
|
174
|
-
raw_type = caps[0].to_s.strip
|
175
|
-
raw_value = caps[1].to_s.strip
|
176
|
-
info[:selector_type] = normalize_type.call(raw_type)
|
177
|
-
info[:selector_value] = raw_value.gsub(/\A['"]|['"]\z/, '')
|
178
|
-
else
|
179
|
-
info[:selector_type] = 'unknown'
|
180
|
-
info[:selector_value] = caps[0].to_s.strip.gsub(/\A['"]|['"]\z/, '')
|
181
|
-
end
|
182
|
-
info[:raw_message] = message[0, 1000]
|
183
|
-
return info
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# tentativa extra: By.<tipo>:<valor> em qualquer lugar da mensagem
|
188
|
-
if (m = message.match(/By\.([a-zA-Z0-9_\- ]+):\s*['"]?(.+?)['"]?/i))
|
189
|
-
info[:selector_type] = normalize_type.call(m[1])
|
190
|
-
info[:selector_value] = m[2].to_s.strip
|
191
|
-
info[:raw_message] = message[0,1000]
|
192
|
-
return info
|
193
|
-
end
|
194
|
-
|
195
|
-
# fallback final: retorna a mensagem inteira recortada (útil para debug)
|
196
|
-
info[:selector_type] = 'unknown'
|
197
|
-
info[:selector_value] = message.strip[0, 500]
|
198
|
-
info[:raw_message] = message[0,1000]
|
199
|
-
info
|
200
|
-
end
|
201
|
-
|
202
|
-
|
203
|
-
def self.find_similar_elements(doc, failed_info, platform)
|
204
|
-
similar_elements = []
|
205
|
-
doc.xpath('//*').each do |node|
|
206
|
-
next if node.name == 'hierarchy'
|
207
|
-
attrs = node.attributes.transform_values(&:value)
|
208
|
-
|
209
|
-
selector_value = failed_info[:selector_value].to_s.downcase.strip
|
210
|
-
|
211
|
-
is_similar = case platform
|
212
|
-
when 'android'
|
213
|
-
(attrs['resource-id']&.downcase&.include?(selector_value) ||
|
214
|
-
attrs['text']&.downcase&.include?(selector_value) ||
|
215
|
-
attrs['content-desc']&.downcase&.include?(selector_value))
|
216
|
-
when 'ios'
|
217
|
-
(attrs['accessibility-id']&.downcase&.include?(selector_value) ||
|
218
|
-
attrs['label']&.downcase&.include?(selector_value) ||
|
219
|
-
attrs['name']&.downcase&.include?(selector_value))
|
220
|
-
else
|
221
|
-
false
|
222
|
-
end
|
223
|
-
|
224
|
-
if is_similar
|
225
|
-
name = self.suggest_name(node.name, attrs)
|
226
|
-
locators = self.xpath_generator(node.name, attrs, platform)
|
227
|
-
similar_elements << { name: name, locators: locators }
|
228
|
-
end
|
229
|
-
end
|
230
|
-
similar_elements
|
231
|
-
end
|
232
|
-
|
233
|
-
def self.truncate(value)
|
234
|
-
return value unless value.is_a?(String)
|
235
|
-
value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
|
236
|
-
end
|
237
|
-
|
238
|
-
def self.suggest_name(tag, attrs)
|
239
|
-
type = tag.split('.').last
|
240
|
-
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
241
|
-
name_base = nil
|
242
|
-
|
243
|
-
['content-desc', 'text', 'resource-id', 'label', 'name'].each do |attr_key|
|
244
|
-
value = attrs[attr_key]
|
245
|
-
if value.is_a?(String) && !value.empty?
|
246
|
-
name_base = value
|
247
|
-
break
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
name_base ||= type
|
252
|
-
|
253
|
-
truncated_name = truncate(name_base)
|
254
|
-
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
255
|
-
|
256
|
-
"#{pfx}#{sanitized_name}"
|
257
|
-
end
|
258
|
-
|
259
|
-
def self.xpath_generator(tag, attrs, platform)
|
260
|
-
case platform
|
261
|
-
when 'android'
|
262
|
-
self.generate_android_xpaths(tag, attrs)
|
263
|
-
when 'ios'
|
264
|
-
self.generate_ios_xpaths(tag, attrs)
|
265
|
-
else
|
266
|
-
self.generate_unknown_xpaths(tag, attrs)
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
def self.generate_android_xpaths(tag, attrs)
|
271
|
-
locators = []
|
272
|
-
|
273
|
-
if attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['text'] && !attrs['text'].empty?
|
274
|
-
locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @text=\"#{self.truncate(attrs['text'])}\"]" }
|
275
|
-
elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
|
276
|
-
locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
277
|
-
end
|
278
|
-
|
279
|
-
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
280
|
-
locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\"]" }
|
281
|
-
end
|
282
|
-
|
283
|
-
if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
|
284
|
-
id_part = attrs['resource-id'].split(':id/').last
|
285
|
-
locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, \"#{id_part}\")]" }
|
286
|
-
end
|
287
|
-
|
288
|
-
if attrs['text'] && !attrs['text'].empty?
|
289
|
-
locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
|
290
|
-
end
|
291
|
-
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
292
|
-
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
293
|
-
end
|
294
|
-
|
295
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
296
|
-
|
297
|
-
locators
|
298
|
-
end
|
299
|
-
|
300
|
-
def self.generate_ios_xpaths(tag, attrs)
|
301
|
-
locators = []
|
302
|
-
|
303
|
-
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty? && attrs['label'] && !attrs['label'].empty?
|
304
|
-
locators << { strategy: 'accessibility_id_and_label', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\" and @label=\"#{self.truncate(attrs['label'])}\"]" }
|
305
|
-
end
|
306
|
-
|
307
|
-
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
|
308
|
-
locators << { strategy: 'accessibility_id', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\"]" }
|
309
|
-
end
|
310
|
-
|
311
|
-
if attrs['label'] && !attrs['label'].empty?
|
312
|
-
locators << { strategy: 'label', locator: "//#{tag}[@label=\"#{self.truncate(attrs['label'])}\"]" }
|
313
|
-
end
|
314
|
-
if attrs['name'] && !attrs['name'].empty?
|
315
|
-
locators << { strategy: 'name', locator: "//#{tag}[@name=\"#{self.truncate(attrs['name'])}\"]" }
|
316
|
-
end
|
317
|
-
|
318
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
319
|
-
|
320
|
-
locators
|
321
|
-
end
|
322
|
-
|
323
|
-
def self.generate_unknown_xpaths(tag, attrs)
|
324
|
-
locators = []
|
325
|
-
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
326
|
-
locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\"]" }
|
327
|
-
end
|
328
|
-
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
329
|
-
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
330
|
-
end
|
331
|
-
if attrs['text'] && !attrs['text'].empty?
|
332
|
-
locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
|
333
|
-
end
|
334
|
-
|
335
|
-
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
336
|
-
|
337
|
-
locators
|
338
|
-
end
|
339
|
-
|
340
|
-
def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
|
341
|
-
|
342
|
-
locators_html = lambda do |locators|
|
343
|
-
locators.map do |loc|
|
344
|
-
"<li class='flex justify-between items-center bg-gray-50 p-2 rounded-md mb-1 text-xs font-mono'><span class='font-bold text-indigo-600'>#{loc[:strategy].upcase.gsub('_', ' ')}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{loc[:locator]}</span></li>"
|
345
|
-
end.join
|
346
|
-
end
|
347
|
-
|
348
|
-
all_elements_html = lambda do |elements|
|
349
|
-
elements.map do |el|
|
350
|
-
"<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{el[:name]}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>"
|
351
|
-
end.join
|
352
|
-
end
|
353
|
-
|
354
|
-
failed_info = targeted_report[:failed_element]
|
355
|
-
similar_elements = targeted_report[:similar_elements]
|
356
|
-
|
357
|
-
similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada. O elemento pode ter sido removido.</p>" : similar_elements.map { |el| "<div class='border border-indigo-100 p-3 rounded-lg bg-indigo-50'><p class='font-bold text-indigo-800 mb-2'>#{el[:name]}</p><ul>#{locators_html.call(el[:locators])}</ul></div>" }.join
|
358
|
-
|
359
|
-
failed_info_content = if failed_info && failed_info[:selector_value]
|
360
|
-
"<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{failed_info[:selector_type]}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{failed_info[:selector_value]}</span></p>"
|
361
|
-
else
|
362
|
-
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
|
363
|
-
end
|
364
|
-
|
365
|
-
# Template HTML usando um heredoc
|
366
|
-
<<~HTML_REPORT
|
367
|
-
<!DOCTYPE html>
|
368
|
-
<html lang="pt-BR">
|
369
|
-
<head>
|
370
|
-
<meta charset="UTF-8">
|
371
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
372
|
-
<title>Relatório de Falha Appium - #{timestamp}</title>
|
373
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
374
|
-
<style>
|
375
|
-
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
376
|
-
.tab-content { display: none; }
|
377
|
-
.tab-content.active { display: block; }
|
378
|
-
.tab-button.active { background-color: #4f46e5; color: white; }
|
379
|
-
.tab-button:not(.active):hover { background-color: #e0e7ff; }
|
380
|
-
</style>
|
381
|
-
</head>
|
382
|
-
<body class="bg-gray-50 p-8">
|
383
|
-
<div class="max-w-7xl mx-auto">
|
384
|
-
<header class="mb-8 pb-4 border-b border-gray-300">
|
385
|
-
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
386
|
-
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
|
387
|
-
</header>
|
388
|
-
|
389
|
-
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
390
|
-
<!-- Coluna de Screenshots e Falha -->
|
391
|
-
<div class="lg:col-span-1">
|
392
|
-
<div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
|
393
|
-
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
394
|
-
#{failed_info_content}
|
395
|
-
</div>
|
396
|
-
|
397
|
-
<div class="bg-white p-4 rounded-lg shadow-xl">
|
398
|
-
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
399
|
-
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
400
|
-
</div>
|
401
|
-
</div>
|
402
|
-
|
403
|
-
<!-- Coluna de Relatórios e Sugestões -->
|
404
|
-
<div class="lg:col-span-2">
|
405
|
-
<div class="bg-white rounded-lg shadow-xl">
|
406
|
-
<!-- Abas de Navegação -->
|
407
|
-
<div class="flex border-b border-gray-200">
|
408
|
-
<button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{similar_elements.size})</button>
|
409
|
-
<button class="tab-button px-4 py-3 text-sm font-medium text-gray-600" data-tab="all">Dump Completo da Página (#{all_suggestions.size} Elementos)</button>
|
410
|
-
</div>
|
411
|
-
|
412
|
-
<!-- Conteúdo das Abas -->
|
413
|
-
<div class="p-6">
|
414
|
-
<!-- Aba Sugestões de Reparo -->
|
415
|
-
<div id="similar" class="tab-content active">
|
416
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
|
417
|
-
<div class="space-y-4">
|
418
|
-
#{similar_elements_content}
|
419
|
-
</div>
|
420
|
-
</div>
|
421
|
-
|
422
|
-
<!-- Aba Dump Completo -->
|
423
|
-
<div id="all" class="tab-content">
|
424
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
|
425
|
-
<div class="max-h-[600px] overflow-y-auto space-y-2">
|
426
|
-
#{all_elements_html.call(all_suggestions)}
|
427
|
-
</div>
|
428
|
-
</div>
|
429
|
-
</div>
|
430
|
-
</div>
|
431
|
-
</div>
|
432
|
-
</div>
|
433
|
-
</div>
|
434
|
-
|
435
|
-
<script>
|
436
|
-
document.addEventListener('DOMContentLoaded', () => {
|
437
|
-
const tabs = document.querySelectorAll('.tab-button');
|
438
|
-
const contents = document.querySelectorAll('.tab-content');
|
439
|
-
|
440
|
-
tabs.forEach(tab => {
|
441
|
-
tab.addEventListener('click', () => {
|
442
|
-
const target = tab.getAttribute('data-tab');
|
443
|
-
|
444
|
-
tabs.forEach(t => {
|
445
|
-
t.classList.remove('active', 'text-white', 'bg-indigo-600');
|
446
|
-
t.classList.add('text-gray-600');
|
447
|
-
});
|
448
|
-
contents.forEach(c => c.classList.remove('active'));
|
449
|
-
|
450
|
-
tab.classList.add('active', 'text-white', 'bg-indigo-600');
|
451
|
-
tab.classList.remove('text-gray-600');
|
452
|
-
document.getElementById(target).classList.add('active');
|
453
|
-
});
|
454
|
-
});
|
455
|
-
|
456
|
-
// Set initial active state for styling consistency
|
457
|
-
const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
|
458
|
-
activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
|
459
|
-
});
|
460
|
-
</script>
|
461
|
-
</body>
|
462
|
-
</html>
|
463
|
-
HTML_REPORT
|
464
|
-
end
|
465
|
-
end
|
466
|
-
end
|