appium_failure_helper 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a56eb0f75acb977e0d8e066b31593613b5ef167675cddb797fd474c152965cc3
|
4
|
+
data.tar.gz: 2c3bf1de88d8976d9fd49622c4c1ae3523dd16f4afe6c0fef764a443edb255a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 645ec5e70207e9345350ea66f64524db344c65cee481ec3aa189c8133557433bd504eca618eeba8aa56c7bb7517cc36170493c56631d5efc3dadc6c6725e99b6
|
7
|
+
data.tar.gz: 3ee2904b1aae5428e5eec0edccdf9565284f147da00206ef6c74a7a2d1b79c0764fc51385ca29c5e63b31f205566c0c9ffb8452bf76afbe24559bf09981b3032
|
@@ -13,67 +13,113 @@ module AppiumFailureHelper
|
|
13
13
|
|
14
14
|
def call
|
15
15
|
begin
|
16
|
-
unless @driver && @driver.session_id
|
16
|
+
unless @driver && @driver.respond_to?(:session_id) && @driver.session_id
|
17
17
|
Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
|
18
18
|
return
|
19
19
|
end
|
20
20
|
|
21
21
|
FileUtils.mkdir_p(@output_folder)
|
22
22
|
|
23
|
-
triage_result = Analyzer.triage_error(@exception)
|
24
|
-
platform_value = @driver.capabilities[:platform_name] || @driver.capabilities['platformName']
|
23
|
+
triage_result = Analyzer.triage_error(@exception) rescue :unknown
|
24
|
+
platform_value = (@driver.capabilities[:platform_name] rescue nil) || (@driver.capabilities['platformName'] rescue nil)
|
25
25
|
platform = platform_value&.downcase || 'unknown'
|
26
26
|
|
27
27
|
report_data = {
|
28
|
-
exception: @exception,
|
29
|
-
|
30
|
-
|
28
|
+
exception: @exception,
|
29
|
+
triage_result: triage_result,
|
30
|
+
timestamp: @timestamp,
|
31
|
+
platform: platform,
|
32
|
+
screenshot_base_64: safe_screenshot_base64
|
31
33
|
}
|
32
34
|
|
33
35
|
if triage_result == :locator_issue
|
34
|
-
page_source =
|
35
|
-
|
36
|
+
page_source = safe_page_source
|
37
|
+
# tenta extrair detalhes do Analyzer (se existir), senão usa fetch_failed_element
|
38
|
+
failed_info = {}
|
39
|
+
begin
|
40
|
+
failed_info = Analyzer.extract_failure_details(@exception) if Analyzer.respond_to?(:extract_failure_details)
|
41
|
+
rescue
|
42
|
+
failed_info = {}
|
43
|
+
end
|
36
44
|
|
37
45
|
if failed_info.nil? || failed_info.empty?
|
38
|
-
|
46
|
+
# tenta extrair do próprio handler (regex mais robusta)
|
47
|
+
failed_info = fetch_failed_element || {}
|
39
48
|
end
|
40
49
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
# fallback para extrair do código-fonte (se existir)
|
51
|
+
if (failed_info.nil? || failed_info.empty?) && SourceCodeAnalyzer.respond_to?(:extract_from_exception)
|
52
|
+
begin
|
53
|
+
failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
|
54
|
+
rescue
|
55
|
+
failed_info = {}
|
56
|
+
end
|
57
|
+
end
|
49
58
|
|
50
|
-
|
51
|
-
|
59
|
+
# garante que exista ao menos um objeto failed_element
|
60
|
+
if failed_info.nil? || failed_info.empty?
|
61
|
+
failed_info = { selector_type: 'unknown', selector_value: @exception&.message.to_s }
|
62
|
+
report_data[:triage_result] = :unidentified_locator_issue
|
63
|
+
end
|
52
64
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
65
|
+
all_page_elements = []
|
66
|
+
best_candidate_analysis = nil
|
67
|
+
alternative_xpaths = []
|
68
|
+
|
69
|
+
if page_source
|
70
|
+
begin
|
71
|
+
doc = Nokogiri::XML(page_source)
|
72
|
+
page_analyzer = PageAnalyzer.new(page_source, platform)
|
73
|
+
all_page_elements = page_analyzer.analyze || []
|
74
|
+
best_candidate_analysis = Analyzer.perform_advanced_analysis(failed_info, all_page_elements, platform) rescue nil
|
75
|
+
rescue => e
|
76
|
+
Utils.logger.warn("Erro analisando page_source: #{e.message}")
|
77
|
+
end
|
78
|
+
end
|
59
79
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
80
|
+
# se não encontrou candidato, gera alternativas a partir do locator bruto
|
81
|
+
if best_candidate_analysis.nil?
|
82
|
+
# tenta parse por Analyzer (se exposto), senão regex fallback
|
83
|
+
failed_attrs = {}
|
84
|
+
begin
|
85
|
+
if Analyzer.respond_to?(:parse_locator) || Analyzer.private_methods.include?(:parse_locator)
|
86
|
+
failed_attrs = Analyzer.send(:parse_locator, failed_info[:selector_type], failed_info[:selector_value], platform) rescue {}
|
64
87
|
end
|
88
|
+
rescue
|
89
|
+
failed_attrs = {}
|
65
90
|
end
|
66
91
|
|
67
|
-
|
92
|
+
if failed_attrs.nil? || failed_attrs.empty?
|
93
|
+
failed_attrs = parse_attrs_from_locator_string(failed_info[:selector_value] || '')
|
94
|
+
end
|
68
95
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
96
|
+
if failed_attrs && !failed_attrs.empty?
|
97
|
+
temp_doc = Nokogiri::XML::Document.new
|
98
|
+
tag = (failed_attrs.delete('tag') || failed_attrs.delete(:tag) || 'element').to_s
|
99
|
+
target_node = Nokogiri::XML::Node.new(tag, temp_doc)
|
100
|
+
failed_attrs.each { |k, v| target_node[k.to_s] = v.to_s unless k.to_s == 'tag' }
|
101
|
+
alternative_xpaths = XPathFactory.generate_for_node(target_node) || []
|
102
|
+
end
|
103
|
+
else
|
104
|
+
# se encontrou candidato, tenta gerar alternativas a partir do node encontrado
|
105
|
+
if best_candidate_analysis[:attributes] && (path = best_candidate_analysis[:attributes][:path])
|
106
|
+
begin
|
107
|
+
doc = Nokogiri::XML(page_source) unless defined?(doc) && doc
|
108
|
+
target_node = doc.at_xpath(path) rescue nil
|
109
|
+
alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
|
110
|
+
rescue
|
111
|
+
# ignore, já temos best_candidate_analysis
|
112
|
+
end
|
113
|
+
end
|
76
114
|
end
|
115
|
+
|
116
|
+
report_data.merge!({
|
117
|
+
page_source: page_source,
|
118
|
+
failed_element: failed_info,
|
119
|
+
best_candidate_analysis: best_candidate_analysis,
|
120
|
+
alternative_xpaths: alternative_xpaths,
|
121
|
+
all_page_elements: all_page_elements
|
122
|
+
})
|
77
123
|
end
|
78
124
|
|
79
125
|
ReportGenerator.new(@output_folder, report_data).generate_all
|
@@ -86,93 +132,73 @@ module AppiumFailureHelper
|
|
86
132
|
|
87
133
|
private
|
88
134
|
|
135
|
+
def safe_screenshot_base64
|
136
|
+
@driver.respond_to?(:screenshot_as) ? @driver.screenshot_as(:base64) : nil
|
137
|
+
rescue => _
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
141
|
+
def safe_page_source
|
142
|
+
return nil unless @driver.respond_to?(:page_source)
|
143
|
+
@driver.page_source
|
144
|
+
rescue => _
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
89
148
|
def fetch_failed_element
|
90
149
|
msg = @exception&.message.to_s
|
91
150
|
|
92
|
-
|
93
|
-
|
151
|
+
# 1) tentativa de parse clássico com aspas (mais restritivo)
|
152
|
+
if (m = msg.match(/using\s+['"](?<type>[^'"]+)['"]\s+with\s+value\s+['"](?<value>.*?)['"]/m))
|
153
|
+
return { selector_type: m[:type], selector_value: m[:value] }
|
154
|
+
end
|
155
|
+
|
156
|
+
# 2) fallback: pega anything após 'with value' até o final da linha (remove quotes extras)
|
157
|
+
if (m = msg.match(/with\s+value\s+(?<value>.+)$/mi))
|
158
|
+
raw = m[:value].strip
|
159
|
+
# remove quotes de borda apenas se existirem
|
160
|
+
raw = raw[1..-2] if raw.start_with?('"', "'") && raw.end_with?('"', "'")
|
161
|
+
# tenta detectar o tipo (xpath, id, accessibility id, css)
|
162
|
+
guessed_type = if raw =~ %r{^//|^/}i
|
163
|
+
'xpath'
|
164
|
+
elsif raw =~ /^[a-zA-Z0-9\-_:.]+:/
|
165
|
+
'id'
|
166
|
+
else
|
167
|
+
(msg[/\b(xpath|id|accessibility id|css)\b/i] || 'unknown').downcase
|
168
|
+
end
|
169
|
+
return { selector_type: guessed_type, selector_value: raw }
|
94
170
|
end
|
95
171
|
|
172
|
+
# 3) outros formatos JSON-like
|
96
173
|
if (m = msg.match(/"method"\s*:\s*"([^"]+)"[\s,}].*"selector"\s*:\s*"([^"]+)"/i))
|
97
174
|
return { selector_type: m[1], selector_value: m[2] }
|
98
175
|
end
|
99
176
|
|
177
|
+
# 4) tentativa simples: pegar primeira ocorrência entre aspas
|
100
178
|
if (m = msg.match(/["']([^"']+)["']/))
|
101
179
|
maybe_value = m[1]
|
102
|
-
unified_map = ElementRepository.load_all rescue {}
|
103
|
-
found = find_in_element_repository_by_value(maybe_value, unified_map)
|
104
|
-
if found
|
105
|
-
return found
|
106
|
-
end
|
107
|
-
|
108
180
|
guessed_type = msg[/\b(xpath|id|accessibility id|css)\b/i] ? $&.downcase : nil
|
109
181
|
return { selector_type: guessed_type || 'unknown', selector_value: maybe_value }
|
110
182
|
end
|
111
183
|
|
112
|
-
|
113
|
-
code_info = SourceCodeAnalyzer.extract_from_exception(@exception) rescue {}
|
114
|
-
unless code_info.nil? || code_info.empty?
|
115
|
-
return code_info
|
116
|
-
end
|
117
|
-
rescue => _; end
|
118
|
-
|
119
|
-
unified_map = ElementRepository.load_all rescue {}
|
120
|
-
unified_map.each do |k, v|
|
121
|
-
k_str = k.to_s.downcase
|
122
|
-
if msg.downcase.include?(k_str)
|
123
|
-
return normalize_repo_element(v)
|
124
|
-
end
|
125
|
-
vals = []
|
126
|
-
if v.is_a?(Hash)
|
127
|
-
vals << v['valor'] if v.key?('valor')
|
128
|
-
vals << v['value'] if v.key?('value')
|
129
|
-
vals << v[:valor] if v.key?(:valor)
|
130
|
-
vals << v[:value] if v.key?(:value)
|
131
|
-
end
|
132
|
-
vals.compact!
|
133
|
-
vals.each do |vv|
|
134
|
-
if vv.to_s.downcase == vv.to_s.downcase && msg.downcase.include?(vv.to_s.downcase)
|
135
|
-
return normalize_repo_element(v)
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
# final fallback
|
141
|
-
debug_log("fetch_failed_element: fallback unknown")
|
142
|
-
{ selector_type: 'unknown', selector_value: 'unknown' }
|
184
|
+
{}
|
143
185
|
end
|
144
186
|
|
145
|
-
def
|
146
|
-
|
147
|
-
|
148
|
-
map.each do |k, v|
|
149
|
-
entry = v.is_a?(Hash) ? v : (v.respond_to?(:to_h) ? v.to_h : nil)
|
150
|
-
next unless entry
|
151
|
-
entry_val = entry['valor'] || entry['value'] || entry[:valor] || entry[:value] || entry['locator'] || entry[:locator]
|
152
|
-
next unless entry_val
|
153
|
-
return normalize_repo_element(entry) if entry_val.to_s.downcase.strip == normalized_value
|
154
|
-
end
|
155
|
-
nil
|
156
|
-
end
|
187
|
+
def parse_attrs_from_locator_string(selector_value)
|
188
|
+
attrs = {}
|
189
|
+
return attrs unless selector_value.is_a?(String) && !selector_value.empty?
|
157
190
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
valor = entry['valor'] || entry[:value] || entry[:locator] || entry[:valor_final] || entry[:value_final]
|
162
|
-
return nil unless valor
|
163
|
-
{ selector_type: (tipo || 'unknown'), selector_value: valor.to_s }
|
164
|
-
end
|
191
|
+
selector_value.scan(/@([a-zA-Z0-9\-\:]+)\s*=\s*['"]([^'"]+)['"]/).each do |k, v|
|
192
|
+
attrs[k] = v
|
193
|
+
end
|
165
194
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
|
171
|
-
target_node = doc.at_xpath(target_path) rescue nil
|
172
|
-
alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
|
173
|
-
end
|
195
|
+
if selector_value =~ %r{//\s*([a-zA-Z0-9_\-:]+)}
|
196
|
+
attrs['tag'] = $1
|
197
|
+
elsif selector_value =~ /^([a-zA-Z0-9_\-:]+)\[/
|
198
|
+
attrs['tag'] = $1
|
174
199
|
end
|
175
|
-
|
200
|
+
|
201
|
+
attrs
|
176
202
|
end
|
177
203
|
end
|
178
204
|
end
|
@@ -24,7 +24,8 @@ module AppiumFailureHelper
|
|
24
24
|
exception_class: @data[:exception].class.to_s,
|
25
25
|
exception_message: @data[:exception].message,
|
26
26
|
failed_element: @data[:failed_element],
|
27
|
-
best_candidate_analysis: @data[:best_candidate_analysis]
|
27
|
+
best_candidate_analysis: @data[:best_candidate_analysis],
|
28
|
+
alternative_xpaths: @data[:alternative_xpaths] || []
|
28
29
|
}
|
29
30
|
File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
|
30
31
|
|
@@ -4,8 +4,18 @@ module AppiumFailureHelper
|
|
4
4
|
|
5
5
|
def self.generate_for_node(node)
|
6
6
|
return [] unless node
|
7
|
+
|
8
|
+
# Se vier um Document, usa o root element
|
9
|
+
if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Document)
|
10
|
+
node = node.root
|
11
|
+
end
|
12
|
+
|
13
|
+
# Só continua se for um elemento que suporta atributos
|
14
|
+
return [] unless node && node.respond_to?(:attributes) && node.element?
|
15
|
+
|
7
16
|
tag = node.name
|
8
|
-
attrs = node.attributes.transform_values(
|
17
|
+
attrs = (node.attributes || {}).transform_values { |a| a.respond_to?(:value) ? a.value : a }
|
18
|
+
|
9
19
|
strategies = []
|
10
20
|
|
11
21
|
add_direct_attribute_strategies(strategies, tag, attrs)
|
@@ -15,7 +25,12 @@ module AppiumFailureHelper
|
|
15
25
|
add_partial_text_strategies(strategies, tag, attrs)
|
16
26
|
add_boolean_strategies(strategies, tag, attrs)
|
17
27
|
add_positional_strategies(strategies, node)
|
18
|
-
|
28
|
+
|
29
|
+
# fallback: se nenhuma estratégia foi criada, garante ao menos o caminho absoluto
|
30
|
+
if strategies.empty?
|
31
|
+
strategies << { name: "Caminho Absoluto (fallback)", strategy: 'xpath', locator: node.path.to_s, reliability: :baixa }
|
32
|
+
end
|
33
|
+
|
19
34
|
strategies.uniq { |s| s[:locator] }.first(MAX_STRATEGIES)
|
20
35
|
end
|
21
36
|
|
@@ -46,32 +61,40 @@ module AppiumFailureHelper
|
|
46
61
|
|
47
62
|
def self.add_parent_based_strategies(strategies, tag, node)
|
48
63
|
parent = node.parent
|
49
|
-
return unless parent
|
64
|
+
return unless parent
|
65
|
+
return if parent.name == 'hierarchy' rescue false
|
50
66
|
|
51
|
-
parent_attrs =
|
67
|
+
parent_attrs = {}
|
68
|
+
if parent.respond_to?(:attributes) && parent.element?
|
69
|
+
parent_attrs = (parent.attributes || {}).transform_values { |a| a.respond_to?(:value) ? a.value : a }
|
70
|
+
end
|
52
71
|
|
53
72
|
if (id = parent_attrs['resource-id']) && !id.empty?
|
54
73
|
strategies << { name: "Filho de Pai com ID", strategy: 'xpath', locator: "//*[@resource-id='#{id}']//#{tag}", reliability: :alta }
|
55
74
|
else
|
56
75
|
parent_attrs.each do |k, v|
|
57
|
-
next if v.empty?
|
76
|
+
next if v.to_s.strip.empty?
|
58
77
|
strategies << { name: "Filho de Pai com #{k}", strategy: 'xpath', locator: "//*[@#{k}='#{v}']//#{tag}", reliability: :media }
|
59
78
|
end
|
60
79
|
end
|
61
80
|
end
|
62
|
-
|
81
|
+
|
63
82
|
def self.add_relational_strategies(strategies, node)
|
64
|
-
|
65
|
-
|
83
|
+
prev = node.previous_sibling
|
84
|
+
if prev && prev.respond_to?(:attributes) && prev.element?
|
85
|
+
prev_attrs = (prev.attributes || {}).transform_values { |a| a.respond_to?(:value) ? a.value : a }
|
86
|
+
if (text = prev_attrs['text']) && !text.empty?
|
87
|
+
strategies << { name: "Relativo ao Irmão Anterior", strategy: 'xpath', locator: "//#{prev.name}[@text='#{text}']/following-sibling::#{node.name}[1]", reliability: :media }
|
88
|
+
end
|
66
89
|
end
|
67
90
|
end
|
68
|
-
|
91
|
+
|
69
92
|
def self.add_partial_text_strategies(strategies, tag, attrs)
|
70
93
|
if (text = attrs['text']) && !text.empty? && text.split.size > 1
|
71
94
|
strategies << { name: "Texto Parcial (contains)", strategy: 'xpath', locator: "//#{tag}[contains(@text, '#{text.split.first}')]", reliability: :media }
|
72
95
|
end
|
73
96
|
end
|
74
|
-
|
97
|
+
|
75
98
|
def self.add_boolean_strategies(strategies, tag, attrs)
|
76
99
|
%w[enabled checked selected].each do |attr|
|
77
100
|
if attrs[attr] == 'true'
|
@@ -81,9 +104,14 @@ module AppiumFailureHelper
|
|
81
104
|
end
|
82
105
|
|
83
106
|
def self.add_positional_strategies(strategies, node)
|
84
|
-
index =
|
107
|
+
index = 1
|
108
|
+
begin
|
109
|
+
index = node.xpath('preceding-sibling::' + node.name).count + 1
|
110
|
+
rescue
|
111
|
+
index = 1
|
112
|
+
end
|
85
113
|
strategies << { name: "Índice na Tela (Frágil)", strategy: 'xpath', locator: "(//#{node.name})[#{index}]", reliability: :baixa }
|
86
|
-
strategies << { name: "Caminho Absoluto (Não Recomendado)", strategy: 'xpath', locator: node.path, reliability: :baixa }
|
114
|
+
strategies << { name: "Caminho Absoluto (Não Recomendado)", strategy: 'xpath', locator: node.path.to_s, reliability: :baixa }
|
87
115
|
end
|
88
116
|
end
|
89
|
-
end
|
117
|
+
end
|