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: 5fe4f4dcf579db89e79b789c9915b51adab896a9d9586220d0162d65ddbba55c
4
- data.tar.gz: b988c6bbd9571393c371449804e4b37b1b5962273dfd275a1d703324734cb8d5
3
+ metadata.gz: a56eb0f75acb977e0d8e066b31593613b5ef167675cddb797fd474c152965cc3
4
+ data.tar.gz: 2c3bf1de88d8976d9fd49622c4c1ae3523dd16f4afe6c0fef764a443edb255a7
5
5
  SHA512:
6
- metadata.gz: 9e34cabdb17a71b586374f5aca2e086dd0bf060eb56ad250d3594fc2c5d02bf4594d2648d356564027997f66f230dcf2a48ee7bb4ffe0fc362798baac3ef13f9
7
- data.tar.gz: 39435fd489a0fc28719499533d372b46a60b82bdd4df202c9fa84054172bd89ea3a8c9f0920d26c327dedd04bb225d8d10dcd278548d7c2c1732026d0d9d2238
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, triage_result: triage_result,
29
- timestamp: @timestamp, platform: platform,
30
- screenshot_base_64: @driver.screenshot_as(:base64)
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 = @driver.page_source
35
- failed_info = Analyzer.extract_failure_details(@exception)
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
- failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
46
+ # tenta extrair do próprio handler (regex mais robusta)
47
+ failed_info = fetch_failed_element || {}
39
48
  end
40
49
 
41
- if failed_info.empty?
42
- report_data[:triage_result] = :unidentified_locator_issue
43
- else
44
- doc = Nokogiri::XML(page_source)
45
- page_analyzer = PageAnalyzer.new(page_source, platform)
46
- all_page_elements = page_analyzer.analyze || []
47
-
48
- best_candidate_analysis = Analyzer.perform_advanced_analysis(failed_info, all_page_elements, platform)
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
- alternative_xpaths = []
51
- target_node = nil
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
- if best_candidate_analysis
54
- if best_candidate_analysis[:attributes] && (path = best_candidate_analysis[:attributes][:path])
55
- target_node = doc.at_xpath(path)
56
- end
57
- else
58
- failed_attrs = Analyzer.send(:parse_locator, failed_info[:selector_type], failed_info[:selector_value], platform)
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
- unless failed_attrs.empty?
61
- temp_doc = Nokogiri::XML::Document.new
62
- target_node = Nokogiri::XML::Node.new(failed_attrs['tag'] || 'element', temp_doc)
63
- failed_attrs.each { |k, v| target_node[k] = v unless k == 'tag' }
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
- alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
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
- report_data.merge!({
70
- page_source: page_source,
71
- failed_element: failed_info,
72
- best_candidate_analysis: best_candidate_analysis,
73
- alternative_xpaths: alternative_xpaths,
74
- all_page_elements: all_page_elements
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
- if (m = msg.match(/using\s+["']?([^"']+)["']?\s+with\s+value\s+["']([^"']+)["']/i))
93
- return { selector_type: m[1], selector_value: m[2] }
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
- begin
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 find_in_element_repository_by_value(value, map = {})
146
- return nil if value.nil? || value.to_s.strip.empty?
147
- normalized_value = value.to_s.downcase.strip
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
- def normalize_repo_element(entry)
159
- return nil unless entry.is_a?(Hash)
160
- tipo = entry['tipoBusca'] || entry[:tipoBusca] || entry['type'] || entry[:type] || entry['search_type'] || entry[:search]
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
- def generate_alternative_xpaths(similar_elements, doc)
167
- alternative_xpaths = []
168
- if !similar_elements.empty?
169
- target_suggestion = similar_elements.first
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
- alternative_xpaths
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
 
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.8.1"
2
+ VERSION = "1.9.0"
3
3
  end
@@ -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(&:value) || {}
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 && parent.name != 'hierarchy'
64
+ return unless parent
65
+ return if parent.name == 'hierarchy' rescue false
50
66
 
51
- parent_attrs = parent.respond_to?(:attributes) ? parent.attributes.transform_values(&:value) : {}
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
- if (prev_sibling = node.previous_sibling) && (text = prev_sibling['text']) && !text.empty?
65
- strategies << { name: "Relativo ao Irmão Anterior", strategy: 'xpath', locator: "//#{prev_sibling.name}[@text='#{text}']/following-sibling::#{node.name}[1]", reliability: :media }
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 = node.xpath('preceding-sibling::' + node.name).count + 1
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appium_failure_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento