appium_failure_helper 1.13.0 → 1.14.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: 28303c5b227b98d8c4d3ace598ada0f314bfb0506c1b83c91c581f2c91f758ec
4
- data.tar.gz: cd8a27134421f9f1f0491124eeee51c3610e582efd8fcf6ab3a4f4917820deb6
3
+ metadata.gz: 04ea0c4e15827de9ded026389d94197ba0855c1cf47d59d3c884b8b4137b18a8
4
+ data.tar.gz: 05aa2f0a47b71c3e3199ca7c1cd728ac5e063cc233355e352c9a0537e0a035b2
5
5
  SHA512:
6
- metadata.gz: c528dbf20a0234c0e64413b4f532bf97a077330a54547bc74517e06d44b8135269bb7c35ed7e5bdd8fdd07420a57877fe9f2f5cf6931655e8d7ccb80ed19b4f1
7
- data.tar.gz: 468e0209d8dd6318571bd9ffc36fa3c53bdcca929648775d55bd1b187ea4c6bf4ae063923a7b87ca9da0fb9ed1af7e5cd797c75add7d75f88234f2ad660dc662
6
+ metadata.gz: 3de3145ca6f8ba1e9038a99a0e11f592021c87e9466e9441f310d9adc2adacb124102628e7d3566e5c54637985fede7d93d6c678dae373356c17d54771b76e2c
7
+ data.tar.gz: fdb2e49ab24be05d518b54aab6bbfbe085069a93679cf6b84d9116c23b9ac274b850e22a0ff0421703a5de912a45723e55a58ede40f67c09f2f62acb95153a2b
@@ -49,29 +49,24 @@ module AppiumFailureHelper
49
49
  alternative_xpaths = []
50
50
 
51
51
  if page_source && !failed_info.empty?
52
- doc = Nokogiri::XML(page_source)
53
- page_analyzer = PageAnalyzer.new(page_source, platform)
54
- all_page_elements = page_analyzer.analyze || []
55
- best_candidate_analysis = Analyzer.perform_advanced_analysis(failed_info, all_page_elements, platform) rescue nil
56
-
57
- # --- SUA REGRA DE NEGÓCIO INTEGRADA AQUI ---
58
- target_node = nil
59
- if best_candidate_analysis && best_candidate_analysis[:attributes] && (path = best_candidate_analysis[:attributes][:path])
60
- # Se encontrou um candidato, ele é o alvo para a XPathFactory
61
- target_node = doc.at_xpath(path)
52
+ tag_for_factory = nil
53
+ attrs_for_factory = nil
54
+
55
+ if best_candidate_analysis && (attrs = best_candidate_analysis[:attributes])
56
+ # Se encontrou um candidato, usa os atributos dele
57
+ tag_for_factory = attrs['tag']
58
+ attrs_for_factory = attrs
62
59
  else
63
- # Se NÃO encontrou, o alvo é o próprio elemento que falhou
60
+ # Se NÃO encontrou, usa os atributos do seletor que falhou
64
61
  failed_attrs = parse_attrs_from_locator_string(failed_info[:selector_value] || '')
65
62
  if !failed_attrs.empty?
66
- temp_doc = Nokogiri::XML::Document.new
67
- tag = (failed_attrs.delete('tag') || 'element').to_s
68
- target_node = Nokogiri::XML::Node.new(tag, temp_doc)
69
- failed_attrs.each { |k, v| target_node[k.to_s] = v.to_s }
63
+ tag_for_factory = failed_attrs.delete('tag')
64
+ attrs_for_factory = failed_attrs
70
65
  end
71
66
  end
72
67
 
73
- # Gera as estratégias para o alvo, seja ele real ou "fantasma"
74
- alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
68
+ # Gera as estratégias usando apenas tag e atributos
69
+ alternative_xpaths = XPathFactory.generate_for_node(tag_for_factory, attrs_for_factory) if tag_for_factory && attrs_for_factory
75
70
  # -----------------------------------------------
76
71
  end
77
72
 
@@ -22,16 +22,25 @@ module AppiumFailureHelper
22
22
 
23
23
  def analyze
24
24
  all_elements_suggestions = []
25
+
26
+ # O XPath '//*' funciona para ambos os XMLs (Android e iOS)
25
27
  @doc.xpath('//*').each do |node|
26
- next if ['hierarchy', 'AppiumAUT'].include?(node.name)
27
-
28
- attrs = node.attribute_nodes.to_h { |attr| [attr.name, attr.value] }
29
-
30
- attrs['tag'] = node.name
31
- name = suggest_name(node.name, attrs)
32
- locators = XPathFactory.generate_for_node(node)
28
+ next if ['hierarchy', 'AppiumAUT'].include?(node.name)
29
+
30
+ # Forma robusta de extrair TODOS os atributos
31
+ attrs = node.attribute_nodes.to_h { |attr| [attr.name, attr.value] }
32
+
33
+ # Normaliza atributos do iOS para que o Analyzer entenda
34
+ if @platform == 'ios'
35
+ attrs['text'] = attrs['label'] || attrs['value']
36
+ attrs['resource-id'] = attrs['name']
37
+ end
33
38
 
34
- all_elements_suggestions << { name: name, locators: locators, attributes: attrs.merge(path: node.path) }
39
+ attrs['tag'] = node.name
40
+ name = suggest_name(node.name, attrs)
41
+ locators = XPathFactory.generate_for_node(node)
42
+
43
+ all_elements_suggestions << { name: name, locators: locators, attributes: attrs.merge(path: node.path) }
35
44
  end
36
45
  all_elements_suggestions.uniq { |s| s[:attributes][:path] }
37
46
  end
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.13.0"
2
+ VERSION = "1.14.0"
3
3
  end
@@ -2,133 +2,167 @@ module AppiumFailureHelper
2
2
  module XPathFactory
3
3
  MAX_STRATEGIES = 20
4
4
 
5
- def self.generate_for_node(node)
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
-
16
- tag = node.name
17
- attrs = (node.attributes || {}).transform_values { |a| a.respond_to?(:value) ? a.value : a }
18
-
5
+ # --- ALTERAÇÃO PRINCIPAL ---
6
+ # O método agora recebe 'tag' e 'attrs', que é tudo o que ele precisa.
7
+ # Ele não depende mais de um nó Nokogiri complexo.
8
+ def self.generate_for_node(tag, attrs)
19
9
  strategies = []
10
+
11
+ # Garante que os argumentos sejam válidos
12
+ tag = (tag || 'element').to_s
13
+ attrs = attrs || {}
20
14
 
15
+ # Executa todas as lógicas de geração
21
16
  add_direct_attribute_strategies(strategies, tag, attrs)
22
17
  add_combinatorial_strategies(strategies, tag, attrs)
23
- add_parent_based_strategies(strategies, tag, node)
24
- add_relational_strategies(strategies, node)
25
18
  add_partial_text_strategies(strategies, tag, attrs)
26
19
  add_boolean_strategies(strategies, tag, attrs)
27
- add_positional_strategies(strategies, node)
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
-
20
+
21
+ # Remove duplicatas e aplica o limite
34
22
  strategies.uniq { |s| s[:locator] }.first(MAX_STRATEGIES)
35
23
  end
36
24
 
37
25
  private
38
26
 
39
- def self.add_direct_attribute_strategies(strategies, tag, attrs)
40
- if (id = attrs['resource-id']) && !id.empty?
41
- strategies << { name: "Localizar por resource-id (Recomendado)", strategy: 'xpath', locator: "//*[@resource-id='#{id}']", reliability: :alta }
42
- end
43
-
44
- # iOS specific attributes
45
- ios_attrs = %w[name label value]
46
- ios_attrs.each do |attr|
47
- next unless attrs[attr] && !attrs[attr].empty?
48
- strategies << { name: "Atributo iOS: #{attr}", strategy: 'xpath', locator: "//#{tag}[@#{attr}=\"#{attrs[attr]}\"]", reliability: :alta }
49
- strategies << { name: "Atributo iOS contém #{attr}", strategy: 'xpath', locator: "//#{tag}[contains(@#{attr}, \"#{attrs[attr].split.first}\")]", reliability: :media }
50
- strategies << { name: "Texto com Inicial (iOS)", strategy: :xpath, locator: "//#{tag}[starts-with(@label, \"#{attrs[attr]}\" ) or starts-with(@name, \"#{attrs[attr]}\")]", reliability: :alta }
51
- strategies << { name: "Texto com Final (iOS)", strategy: :xpath, locator: "//#{tag}[substring(@label, string-length(@label) - string-length(\"#{attrs[attr]}\") + 1) = \"#{attrs[attr]}\" or substring(@name, string-length(@name) - string-length(\"#{attrs[attr]}\") + 1) = \"#{attrs[attr]}\"]", reliability: :media }
52
- end
27
+ def self.add_direct_attribute_strategies(strategies, tag, attrs)
28
+ # 1. resource-id (Android)
29
+ if (id = attrs['resource-id']) && !id.empty?
30
+ strategies << {
31
+ name: "ID Único (Recomendado)",
32
+ strategy: 'id',
33
+ locator: id,
34
+ reliability: :alta
35
+ }
36
+ end
53
37
 
54
- # Android specific attributes
55
- if (text = attrs['text']) && !text.empty?
56
- strategies << { name: "Texto Exato (Android)", strategy: 'xpath', locator: "//#{tag}[@text=\"#{text}\"]", reliability: :alta }
57
- strategies << { name: "Texto com Inicial (Android)", strategy: 'xpath', locator: "//#{tag}[starts-with(@text, \"#{text}\")]", reliability: :alta }
58
- strategies << { name: "Texto com Final (Android)", strategy: :xpath, locator: "//#{tag}[substring(@text, string-length(@text) - string-length(\"#{text}\") + 1) = \"#{text}\"]", reliability: :media }
59
- strategies << { name: "Texto Parcial (Android)", strategy: 'xpath', locator: "//#{tag}[contains(@text, \"#{text.split.first}\")]", reliability: :media }
38
+ # 2. text exato
39
+ if (text = attrs['text']) && !text.empty?
40
+ strategies << {
41
+ name: "Texto Exato",
42
+ strategy: 'xpath',
43
+ locator: "//#{tag}[@text=#{text.inspect}]",
44
+ reliability: :alta
45
+ }
46
+
47
+ # Texto que contém o valor (útil em traduções ou labels dinâmicos)
48
+ if text.split.size > 1
49
+ strategies << {
50
+ name: "Texto Parcial (contains)",
51
+ strategy: 'xpath',
52
+ locator: "//#{tag}[contains(@text, #{text.split.first.inspect})]",
53
+ reliability: :media
54
+ }
60
55
  end
56
+ end
61
57
 
62
- if (desc = attrs['content-desc']) && !desc.empty?
63
- strategies << { name: "content-desc (Acessibilidade Android)", strategy: 'xpath', locator: "//#{tag}[@content-desc=\"#{desc}\"]", reliability: :alta }
64
- end
58
+ # 3. content-desc (Android)
59
+ if (desc = attrs['content-desc']) && !desc.empty?
60
+ strategies << {
61
+ name: "Content Description",
62
+ strategy: 'accessibility_id',
63
+ locator: desc,
64
+ reliability: :alta
65
+ }
66
+
67
+ # fallback via xpath (às vezes accessibility_id falha em híbridos)
68
+ strategies << {
69
+ name: "Content Description (XPath Fallback)",
70
+ strategy: 'xpath',
71
+ locator: "//#{tag}[@content-desc=#{desc.inspect}]",
72
+ reliability: :media
73
+ }
65
74
  end
66
75
 
76
+ # 4. name (iOS)
77
+ if (name = attrs['name']) && !name.empty?
78
+ strategies << {
79
+ name: "Name (iOS)",
80
+ strategy: 'name',
81
+ locator: name,
82
+ reliability: :alta
83
+ }
84
+ end
67
85
 
68
- def self.add_combinatorial_strategies(strategies, tag, attrs)
69
- valid_attrs = attrs.select { |k, v| %w[text content-desc class package].include?(k) && v && !v.empty? }
70
- return if valid_attrs.keys.size < 2
86
+ # 5. label (iOS)
87
+ if (label = attrs['label']) && !label.empty?
88
+ strategies << {
89
+ name: "Label (iOS)",
90
+ strategy: 'xpath',
91
+ locator: "//#{tag}[@label=#{label.inspect}]",
92
+ reliability: :alta
93
+ }
94
+ end
71
95
 
72
- valid_attrs.keys.combination(2).each do |comb|
73
- locator_parts = comb.map { |k| "@#{k}='#{attrs[k]}'" }.join(' and ')
74
- attr_names = comb.map(&:capitalize).join(' + ')
75
- strategies << { name: "Combinação: #{attr_names}", strategy: 'xpath', locator: "//#{tag}[#{locator_parts}]", reliability: :alta }
76
- end
96
+ # 6. class e index (fallback quando não há IDs)
97
+ if (cls = attrs['class']) && !cls.empty? && (index = attrs['index'])
98
+ strategies << {
99
+ name: "Classe + Índice",
100
+ strategy: 'xpath',
101
+ locator: "(//#{cls})[#{index.to_i + 1}]",
102
+ reliability: :baixa
103
+ }
77
104
  end
105
+ end
78
106
 
79
- def self.add_parent_based_strategies(strategies, tag, node)
80
- parent = node.parent
81
- return unless parent
82
- return if parent.name == 'hierarchy' rescue false
107
+ # ----------------------------------------------
83
108
 
84
- parent_attrs = {}
85
- if parent.respond_to?(:attributes) && parent.element?
86
- parent_attrs = (parent.attributes || {}).transform_values { |a| a.respond_to?(:value) ? a.value : a }
87
- end
109
+ def self.add_combinatorial_strategies(strategies, tag, attrs)
110
+ valid_attrs = attrs.select { |k, v| %w[text content-desc class package label name].include?(k) && v && !v.empty? }
111
+ return if valid_attrs.keys.size < 2
88
112
 
89
- if (id = parent_attrs['resource-id']) && !id.empty?
90
- strategies << { name: "Filho de Pai com ID", strategy: 'xpath', locator: "//*[@resource-id='#{id}']//#{tag}", reliability: :alta }
91
- else
92
- parent_attrs.each do |k, v|
93
- next if v.to_s.strip.empty?
94
- strategies << { name: "Filho de Pai com #{k}", strategy: 'xpath', locator: "//*[@#{k}='#{v}']//#{tag}", reliability: :media }
95
- end
96
- end
97
- end
113
+ valid_attrs.keys.combination(2).each do |comb|
114
+ locator_parts = comb.map { |k| "@#{k}=#{attrs[k].inspect}" }.join(' and ')
115
+ attr_names = comb.map(&:capitalize).join(' + ')
116
+ reliability = comb.include?('class') ? :media : :alta
98
117
 
99
- def self.add_relational_strategies(strategies, node)
100
- prev = node.previous_sibling
101
- if prev && prev.respond_to?(:attributes) && prev.element?
102
- prev_attrs = (prev.attributes || {}).transform_values { |a| a.respond_to?(:value) ? a.value : a }
103
- if (text = prev_attrs['text']) && !text.empty?
104
- strategies << { name: "Relativo ao Irmão Anterior", strategy: 'xpath', locator: "//#{prev.name}[@text='#{text}']/following-sibling::#{node.name}[1]", reliability: :media }
105
- end
106
- end
118
+ strategies << {
119
+ name: "Combinação: #{attr_names}",
120
+ strategy: 'xpath',
121
+ locator: "//#{tag}[#{locator_parts}]",
122
+ reliability: reliability
123
+ }
107
124
  end
125
+ end
108
126
 
109
- def self.add_partial_text_strategies(strategies, tag, attrs)
110
- if (text = attrs['text']) && !text.empty? && text.split.size > 1
111
- strategies << { name: "Texto Parcial (contains)", strategy: 'xpath', locator: "//#{tag}[contains(@text, '#{text.split.first}')]", reliability: :media }
127
+ # ----------------------------------------------
128
+
129
+ def self.add_partial_text_strategies(strategies, tag, attrs)
130
+ if (text = attrs['text']) && !text.empty?
131
+ keywords = text.split.select { |t| t.size > 3 } # ignora palavras curtas
132
+ keywords.first(2).each do |kw|
133
+ strategies << {
134
+ name: "Texto Parcial (#{kw})",
135
+ strategy: 'xpath',
136
+ locator: "//#{tag}[contains(@text, #{kw.inspect})]",
137
+ reliability: :media
138
+ }
112
139
  end
113
140
  end
141
+ end
114
142
 
115
- def self.add_boolean_strategies(strategies, tag, attrs)
116
- %w[enabled checked selected].each do |attr|
117
- if attrs[attr] == 'true'
118
- strategies << { name: "#{attr.capitalize} é Verdadeiro", strategy: 'xpath', locator: "//#{tag}[@#{attr}='true']", reliability: :media }
119
- end
143
+ # ----------------------------------------------
144
+
145
+ def self.add_boolean_strategies(strategies, tag, attrs)
146
+ %w[enabled checked selected clickable focusable focused scrollable long-clickable password].each do |attr|
147
+ if attrs[attr] == 'true'
148
+ reliability = %w[checked selected].include?(attr) ? :alta : :media
149
+ strategies << {
150
+ name: "#{attr.capitalize} é Verdadeiro",
151
+ strategy: 'xpath',
152
+ locator: "//#{tag}[@#{attr}='true']",
153
+ reliability: reliability
154
+ }
120
155
  end
121
156
  end
157
+ end
122
158
 
123
- def self.add_positional_strategies(strategies, node)
124
- index = 1
125
- begin
126
- index = node.xpath('preceding-sibling::' + node.name).count + 1
127
- rescue
128
- index = 1
129
- end
130
- strategies << { name: "Índice na Tela (Frágil)", strategy: 'xpath', locator: "(//#{node.name})[#{index}]", reliability: :baixa }
131
- strategies << { name: "Caminho Absoluto (Não Recomendado)", strategy: 'xpath', locator: node.path.to_s, reliability: :baixa }
132
- end
159
+
160
+ # REMOVIDO: Métodos que dependiam de um nó Nokogiri completo
161
+ # - add_relational_strategies (dependia de .previous_sibling)
162
+ # - add_positional_strategies (dependia de .xpath e .path)
163
+ # - add_parent_based_strategies (dependia de .parent)
164
+ #
165
+ # Estes métodos são inerentemente frágeis e falhavam no nosso cenário de "nó fantasma".
166
+ # As estratégias de atributos diretos e combinatórias são muito mais robustas.
133
167
  end
134
- end
168
+ 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.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento