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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04ea0c4e15827de9ded026389d94197ba0855c1cf47d59d3c884b8b4137b18a8
|
|
4
|
+
data.tar.gz: 05aa2f0a47b71c3e3199ca7c1cd728ac5e063cc233355e352c9a0537e0a035b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
best_candidate_analysis
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
74
|
-
alternative_xpaths = XPathFactory.generate_for_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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
@@ -2,133 +2,167 @@ module AppiumFailureHelper
|
|
|
2
2
|
module XPathFactory
|
|
3
3
|
MAX_STRATEGIES = 20
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
parent = node.parent
|
|
81
|
-
return unless parent
|
|
82
|
-
return if parent.name == 'hierarchy' rescue false
|
|
107
|
+
# ----------------------------------------------
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|