appium_failure_helper 0.4.2 → 0.5.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 +4 -4
- data/README.md +15 -16
- data/lib/appium_failure_helper/capture.rb +249 -29
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7344cd53f36a48ed0689b608758d04724a1f2dc122197171e5b875bb08615fb
|
4
|
+
data.tar.gz: acc66c6049fe82c85dc769591b7a81befda696cc5c4b02e8b8bd99ec60b643ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44d7f03b734112353b8c5c7f0db157f125d54e7d9065471e5caba380c44d851f6a8eb757d410ec0f6280b412fc6b442eeb3b303143e98c0599b7d5adcf3d7792
|
7
|
+
data.tar.gz: 48469708fd272ac232494b32bccbf1a99c4cc277a60cca1296eaf6d29128e55794a16987939a1f89fc3a9d8c2ec37ab9a56686a4edc80e4e4cd73de6948031cb
|
data/README.md
CHANGED
@@ -4,35 +4,34 @@ Este módulo Ruby foi projetado para ser uma ferramenta de diagnóstico intelige
|
|
4
4
|
|
5
5
|
## Funcionalidades Principais
|
6
6
|
|
7
|
-
* **
|
8
|
-
|
9
|
-
* **
|
7
|
+
* **Diagnóstico de Falha Automatizado:** No momento de uma falha, a ferramenta captura automaticamente o estado da aplicação e gera um conjunto de artefatos de depuração.
|
8
|
+
|
9
|
+
* **Captura de Artefatos:** Salva um **screenshot** da tela, o **XML completo do `page_source`**, e um relatório de análise em uma pasta com carimbo de data e hora para cada falha.
|
10
|
+
|
11
|
+
* **Geração de Localizadores Inteligente:** Percorre a árvore de elementos e gera um relatório YAML com sugestões de XPaths otimizados para cada elemento.
|
12
|
+
|
10
13
|
* **Lógica de XPath Otimizada:** Utiliza as melhores práticas para cada plataforma (**Android e iOS**), priorizando os localizadores mais estáveis e combinando atributos para alta especificidade.
|
11
|
-
* **Organização de Saída:** Cria uma pasta com um carimbo de data/hora para cada falha (`/failure_AAAA_MM_DD_HHMMSS`), mantendo os arquivos organizados.
|
12
|
-
* **Contexto de Elementos:** O relatório YAML agora inclui o **XPath do elemento pai (`parent_locator`)**, fornecendo contexto crucial para a depuração e construção de Page Objects.
|
13
14
|
|
14
|
-
|
15
|
+
* **Tratamento de Dados:** Trunca valores de atributos muito longos para evitar quebras no relatório e garante que não haja elementos duplicados.
|
15
16
|
|
16
|
-
|
17
|
+
* **Sistema de Logging:** Usa a biblioteca padrão `Logger` do Ruby para fornecer feedback detalhado e limpo no console.
|
17
18
|
|
18
|
-
|
19
|
-
2. Captura o screenshot e o `page_source` do driver.
|
20
|
-
3. Determina a plataforma do dispositivo a partir das capacidades do driver.
|
21
|
-
4. Itera sobre cada nó do `page_source` e, para cada um, chama a lógica de geração de XPath e de nome.
|
22
|
-
5. A lógica de XPath utiliza um conjunto de estratégias priorizadas para cada plataforma, como **combinação de atributos** (`@resource-id` e `@text`) e o uso de `starts-with()` para elementos dinâmicos.
|
23
|
-
6. Salva um arquivo `.yaml` estruturado, contendo o nome sugerido, o tipo (`xpath`) e o localizador para cada elemento.
|
19
|
+
* **Múltiplos Relatórios:** Gera dois arquivos YAML: um **focado** no elemento que falhou e um **completo** com todos os elementos da tela.
|
24
20
|
|
25
|
-
|
21
|
+
* **Relatório HTML Interativo:** Cria um relatório HTML visualmente agradável que une o screenshot, o XML e os localizadores sugeridos em uma única página para fácil acesso e análise.
|
26
22
|
|
27
|
-
|
23
|
+
## Como Funciona
|
24
|
+
|
25
|
+
O `AppiumFailureHelper` deve ser integrado ao seu framework de testes. Um exemplo comum é utilizá-lo em um hook `After` do Cucumber, passando o objeto de driver do Appium e a exceção do cenário.
|
28
26
|
|
29
27
|
**`features/support/hooks.rb`**
|
28
|
+
|
30
29
|
```ruby
|
31
30
|
require 'appium_failure_helper'
|
32
31
|
|
33
32
|
After do |scenario|
|
34
33
|
if scenario.failed?
|
35
|
-
AppiumFailureHelper::Capture.handler_failure(appium_driver)
|
34
|
+
AppiumFailureHelper::Capture.handler_failure(appium_driver, scenario.exception)
|
36
35
|
end
|
37
36
|
end
|
38
37
|
|
@@ -2,6 +2,7 @@ require 'nokogiri'
|
|
2
2
|
require 'fileutils'
|
3
3
|
require 'base64'
|
4
4
|
require 'yaml'
|
5
|
+
require 'logger'
|
5
6
|
|
6
7
|
module AppiumFailureHelper
|
7
8
|
class Capture
|
@@ -32,59 +33,267 @@ module AppiumFailureHelper
|
|
32
33
|
}.freeze
|
33
34
|
|
34
35
|
MAX_VALUE_LENGTH = 100
|
36
|
+
@@logger = nil
|
35
37
|
|
36
|
-
def self.handler_failure(driver)
|
38
|
+
def self.handler_failure(driver, exception)
|
37
39
|
begin
|
40
|
+
self.setup_logger unless @@logger
|
41
|
+
|
42
|
+
# Remove a pasta reports_failure ao iniciar uma nova execução
|
43
|
+
FileUtils.rm_rf("reports_failure")
|
44
|
+
@@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
|
45
|
+
|
38
46
|
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
39
|
-
output_folder = "
|
47
|
+
output_folder = "reports_failure/failure_#{timestamp}"
|
40
48
|
|
41
49
|
FileUtils.mkdir_p(output_folder)
|
50
|
+
@@logger.info("Pasta de saída criada: #{output_folder}")
|
42
51
|
|
52
|
+
# Captura o Base64 e salva o PNG
|
53
|
+
screenshot_base64 = driver.screenshot_as(:base64)
|
43
54
|
screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
|
44
55
|
File.open(screenshot_path, 'wb') do |f|
|
45
|
-
f.write(Base64.decode64(
|
56
|
+
f.write(Base64.decode64(screenshot_base64))
|
46
57
|
end
|
47
|
-
|
58
|
+
@@logger.info("Screenshot salvo em #{screenshot_path}")
|
48
59
|
|
49
60
|
page_source = driver.page_source
|
50
61
|
xml_path = "#{output_folder}/page_source_#{timestamp}.xml"
|
51
62
|
File.write(xml_path, page_source)
|
63
|
+
@@logger.info("Page source salvo em #{xml_path}")
|
52
64
|
|
53
65
|
doc = Nokogiri::XML(page_source)
|
54
|
-
|
55
66
|
platform = driver.capabilities['platformName']&.downcase || 'unknown'
|
56
67
|
|
57
|
-
|
58
|
-
suggestions = []
|
68
|
+
failed_element_info = self.extract_info_from_exception(exception)
|
59
69
|
|
70
|
+
# --- Processamento de todos os elementos ---
|
71
|
+
seen_elements = {}
|
72
|
+
all_elements_suggestions = []
|
60
73
|
doc.xpath('//*').each do |node|
|
61
74
|
next if node.name == 'hierarchy'
|
62
75
|
attrs = node.attributes.transform_values(&:value)
|
63
76
|
|
64
|
-
unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
|
77
|
+
unique_key = "#{node.name}|#{attrs['resource-id'].to_s}|#{attrs['content-desc'].to_s}|#{attrs['text'].to_s}"
|
65
78
|
|
66
79
|
unless seen_elements[unique_key]
|
67
80
|
name = self.suggest_name(node.name, attrs)
|
68
81
|
locators = self.xpath_generator(node.name, attrs, platform)
|
69
82
|
|
70
|
-
|
83
|
+
all_elements_suggestions << { name: name, locators: locators }
|
71
84
|
seen_elements[unique_key] = true
|
72
85
|
end
|
73
86
|
end
|
74
87
|
|
75
|
-
|
76
|
-
|
77
|
-
|
88
|
+
# --- Geração do Relatório FOCADO (1) ---
|
89
|
+
targeted_report = {
|
90
|
+
failed_element: failed_element_info,
|
91
|
+
similar_elements: [],
|
92
|
+
}
|
93
|
+
|
94
|
+
if failed_element_info && failed_element_info[:selector_value]
|
95
|
+
targeted_report[:similar_elements] = self.find_similar_elements(doc, failed_element_info, platform)
|
96
|
+
end
|
97
|
+
|
98
|
+
targeted_yaml_path = "#{output_folder}/failure_analysis_#{timestamp}.yaml"
|
99
|
+
File.open(targeted_yaml_path, 'w') do |f|
|
100
|
+
f.write(YAML.dump(targeted_report))
|
101
|
+
end
|
102
|
+
@@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
|
103
|
+
|
104
|
+
# --- Geração do Relatório COMPLETO (2) ---
|
105
|
+
full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
|
106
|
+
File.open(full_dump_yaml_path, 'w') do |f|
|
107
|
+
f.write(YAML.dump(all_elements_suggestions))
|
78
108
|
end
|
109
|
+
@@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
|
110
|
+
|
111
|
+
# --- Geração do Relatório HTML (3) ---
|
112
|
+
html_report_path = "#{output_folder}/report_#{timestamp}.html"
|
113
|
+
html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
|
114
|
+
File.write(html_report_path, html_content)
|
115
|
+
@@logger.info("Relatório HTML completo salvo em #{html_report_path}")
|
116
|
+
|
79
117
|
|
80
|
-
puts "Element suggestions saved to #{yaml_path}"
|
81
118
|
rescue => e
|
82
|
-
|
119
|
+
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
83
120
|
end
|
84
121
|
end
|
85
122
|
|
86
123
|
private
|
87
124
|
|
125
|
+
def self.setup_logger
|
126
|
+
@@logger = Logger.new(STDOUT)
|
127
|
+
@@logger.level = Logger::INFO
|
128
|
+
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
129
|
+
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# --- LÓGICA DE GERAÇÃO DE HTML ---
|
134
|
+
def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
|
135
|
+
|
136
|
+
# Helper para formatar localizadores
|
137
|
+
locators_html = lambda do |locators|
|
138
|
+
locators.map do |loc|
|
139
|
+
"<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>"
|
140
|
+
end.join
|
141
|
+
end
|
142
|
+
|
143
|
+
# Helper para criar a lista de todos os elementos
|
144
|
+
all_elements_html = lambda do |elements|
|
145
|
+
elements.map do |el|
|
146
|
+
"<div class='border-b border-gray-200 py-3'><p class='font-semibold text-sm text-gray-800 mb-1'>#{el[:name]}</p><ul class='text-xs space-y-1'>#{locators_html.call(el[:locators])}</ul></div>"
|
147
|
+
end.join
|
148
|
+
end
|
149
|
+
|
150
|
+
# Template HTML usando um heredoc
|
151
|
+
<<~HTML_REPORT
|
152
|
+
<!DOCTYPE html>
|
153
|
+
<html lang="pt-BR">
|
154
|
+
<head>
|
155
|
+
<meta charset="UTF-8">
|
156
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
157
|
+
<title>Relatório de Falha Appium - #{timestamp}</title>
|
158
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
159
|
+
<style>
|
160
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
161
|
+
.tab-content { display: none; }
|
162
|
+
.tab-content.active { display: block; }
|
163
|
+
.tab-button.active { background-color: #4f46e5; color: white; }
|
164
|
+
.tab-button:not(.active):hover { background-color: #e0e7ff; }
|
165
|
+
</style>
|
166
|
+
</head>
|
167
|
+
<body class="bg-gray-50 p-8">
|
168
|
+
<div class="max-w-7xl mx-auto">
|
169
|
+
<header class="mb-8 pb-4 border-b border-gray-300">
|
170
|
+
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
171
|
+
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
|
172
|
+
</header>
|
173
|
+
|
174
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
175
|
+
<div class="lg:col-span-1">
|
176
|
+
<div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
|
177
|
+
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
178
|
+
<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">#{targeted_report[:failed_element][:selector_type] || 'Desconhecido'}</span></p>
|
179
|
+
<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">#{targeted_report[:failed_element][:selector_value] || 'N/A'}</span></p>
|
180
|
+
</div>
|
181
|
+
|
182
|
+
<div class="bg-white p-4 rounded-lg shadow-xl">
|
183
|
+
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
184
|
+
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
185
|
+
</div>
|
186
|
+
</div>
|
187
|
+
|
188
|
+
<div class="lg:col-span-2">
|
189
|
+
<div class="bg-white rounded-lg shadow-xl">
|
190
|
+
<div class="flex border-b border-gray-200">
|
191
|
+
<button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{targeted_report[:similar_elements].size})</button>
|
192
|
+
<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>
|
193
|
+
</div>
|
194
|
+
|
195
|
+
<div class="p-6">
|
196
|
+
<div id="similar" class="tab-content active">
|
197
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Melhores Alternativas)</h3>
|
198
|
+
#{"<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na página. O elemento pode ter sido removido ou o localizador está incorreto.</p>" if targeted_report[:similar_elements].empty?}
|
199
|
+
<div class="space-y-4">
|
200
|
+
#{targeted_report[: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}
|
201
|
+
</div>
|
202
|
+
</div>
|
203
|
+
|
204
|
+
<div id="all" class="tab-content">
|
205
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
|
206
|
+
<div class="max-h-[600px] overflow-y-auto space-y-2">
|
207
|
+
#{all_elements_html.call(all_suggestions)}
|
208
|
+
</div>
|
209
|
+
</div>
|
210
|
+
</div>
|
211
|
+
</div>
|
212
|
+
</div>
|
213
|
+
</div>
|
214
|
+
</div>
|
215
|
+
|
216
|
+
<script>
|
217
|
+
document.addEventListener('DOMContentLoaded', () => {
|
218
|
+
const tabs = document.querySelectorAll('.tab-button');
|
219
|
+
const contents = document.querySelectorAll('.tab-content');
|
220
|
+
|
221
|
+
tabs.forEach(tab => {
|
222
|
+
tab.addEventListener('click', () => {
|
223
|
+
const target = tab.getAttribute('data-tab');
|
224
|
+
|
225
|
+
tabs.forEach(t => t.classList.remove('active', 'text-white', 'text-gray-600', 'hover:bg-indigo-700'));
|
226
|
+
contents.forEach(c => c.classList.remove('active'));
|
227
|
+
|
228
|
+
tab.classList.add('active', 'text-white', 'bg-indigo-600');
|
229
|
+
document.getElementById(target).classList.add('active');
|
230
|
+
});
|
231
|
+
});
|
232
|
+
// Set initial active state for styling consistency
|
233
|
+
const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
|
234
|
+
activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
|
235
|
+
});
|
236
|
+
</script>
|
237
|
+
</body>
|
238
|
+
</html>
|
239
|
+
HTML_REPORT
|
240
|
+
end
|
241
|
+
|
242
|
+
# --- Métodos de Suporte Existentes ---
|
243
|
+
|
244
|
+
# ... (métodos setup_logger, extract_info_from_exception, find_similar_elements, etc.)
|
245
|
+
|
246
|
+
def self.extract_info_from_exception(exception)
|
247
|
+
message = exception.message
|
248
|
+
info = {}
|
249
|
+
|
250
|
+
patterns = [
|
251
|
+
/(?:could not be found|cannot find element) using (.+)=['"](.+)['"]/i,
|
252
|
+
/no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i
|
253
|
+
]
|
254
|
+
|
255
|
+
patterns.each do |pattern|
|
256
|
+
match = message.match(pattern)
|
257
|
+
if match
|
258
|
+
selector_type = match[1].strip
|
259
|
+
selector_value = match[2].strip
|
260
|
+
|
261
|
+
info[:selector_type] = selector_type
|
262
|
+
info[:selector_value] = selector_value.gsub(/['"]/, '')
|
263
|
+
return info
|
264
|
+
end
|
265
|
+
end
|
266
|
+
info
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.find_similar_elements(doc, failed_info, platform)
|
270
|
+
similar_elements = []
|
271
|
+
doc.xpath('//*').each do |node|
|
272
|
+
next if node.name == 'hierarchy'
|
273
|
+
attrs = node.attributes.transform_values(&:value)
|
274
|
+
|
275
|
+
is_similar = case platform
|
276
|
+
when 'android'
|
277
|
+
(attrs['resource-id']&.include?(failed_info[:selector_value]) ||
|
278
|
+
attrs['text']&.include?(failed_info[:selector_value]) ||
|
279
|
+
attrs['content-desc']&.include?(failed_info[:selector_value]))
|
280
|
+
when 'ios'
|
281
|
+
(attrs['accessibility-id']&.include?(failed_info[:selector_value]) ||
|
282
|
+
attrs['label']&.include?(failed_info[:selector_value]) ||
|
283
|
+
attrs['name']&.include?(failed_info[:selector_value]))
|
284
|
+
else
|
285
|
+
false
|
286
|
+
end
|
287
|
+
|
288
|
+
if is_similar
|
289
|
+
name = self.suggest_name(node.name, attrs)
|
290
|
+
locators = self.xpath_generator(node.name, attrs, platform)
|
291
|
+
similar_elements << { name: name, locators: locators }
|
292
|
+
end
|
293
|
+
end
|
294
|
+
similar_elements
|
295
|
+
end
|
296
|
+
|
88
297
|
def self.truncate(value)
|
89
298
|
return value unless value.is_a?(String)
|
90
299
|
value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
|
@@ -93,9 +302,22 @@ module AppiumFailureHelper
|
|
93
302
|
def self.suggest_name(tag, attrs)
|
94
303
|
type = tag.split('.').last
|
95
304
|
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
96
|
-
|
97
|
-
|
98
|
-
|
305
|
+
name_base = nil
|
306
|
+
|
307
|
+
['content-desc', 'text', 'resource-id', 'label', 'name'].each do |attr_key|
|
308
|
+
value = attrs[attr_key]
|
309
|
+
if value.is_a?(String) && !value.empty?
|
310
|
+
name_base = value
|
311
|
+
break
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
name_base ||= type
|
316
|
+
|
317
|
+
truncated_name = truncate(name_base)
|
318
|
+
sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
|
319
|
+
|
320
|
+
"#{pfx}#{sanitized_name}"
|
99
321
|
end
|
100
322
|
|
101
323
|
def self.xpath_generator(tag, attrs, platform)
|
@@ -112,25 +334,21 @@ module AppiumFailureHelper
|
|
112
334
|
def self.generate_android_xpaths(tag, attrs)
|
113
335
|
locators = []
|
114
336
|
|
115
|
-
# Estratégia 1: Combinação de atributos
|
116
337
|
if attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['text'] && !attrs['text'].empty?
|
117
|
-
locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id
|
338
|
+
locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @text=\"#{self.truncate(attrs['text'])}\"]" }
|
118
339
|
elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
|
119
340
|
locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
120
341
|
end
|
121
342
|
|
122
|
-
# Estratégia 2: ID único
|
123
343
|
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
124
344
|
locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\"]" }
|
125
345
|
end
|
126
346
|
|
127
|
-
# Estratégia 3: starts-with para IDs dinâmicos
|
128
347
|
if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
|
129
348
|
id_part = attrs['resource-id'].split(':id/').last
|
130
349
|
locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, \"#{id_part}\")]" }
|
131
350
|
end
|
132
351
|
|
133
|
-
# Estratégia 4: Texto e content-desc como identificadores
|
134
352
|
if attrs['text'] && !attrs['text'].empty?
|
135
353
|
locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
|
136
354
|
end
|
@@ -138,7 +356,6 @@ module AppiumFailureHelper
|
|
138
356
|
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
|
139
357
|
end
|
140
358
|
|
141
|
-
# Fallback genérico (sempre adicionado)
|
142
359
|
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
143
360
|
|
144
361
|
locators
|
@@ -147,17 +364,14 @@ module AppiumFailureHelper
|
|
147
364
|
def self.generate_ios_xpaths(tag, attrs)
|
148
365
|
locators = []
|
149
366
|
|
150
|
-
# Estratégia 1: Combinação de atributos
|
151
367
|
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty? && attrs['label'] && !attrs['label'].empty?
|
152
368
|
locators << { strategy: 'accessibility_id_and_label', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\" and @label=\"#{self.truncate(attrs['label'])}\"]" }
|
153
369
|
end
|
154
370
|
|
155
|
-
# Estratégia 2: ID único
|
156
371
|
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
|
157
372
|
locators << { strategy: 'accessibility_id', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\"]" }
|
158
373
|
end
|
159
374
|
|
160
|
-
# Estratégia 3: label, name ou value
|
161
375
|
if attrs['label'] && !attrs['label'].empty?
|
162
376
|
locators << { strategy: 'label', locator: "//#{tag}[@label=\"#{self.truncate(attrs['label'])}\"]" }
|
163
377
|
end
|
@@ -165,7 +379,6 @@ module AppiumFailureHelper
|
|
165
379
|
locators << { strategy: 'name', locator: "//#{tag}[@name=\"#{self.truncate(attrs['name'])}\"]" }
|
166
380
|
end
|
167
381
|
|
168
|
-
# Fallback genérico (sempre adicionado)
|
169
382
|
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
170
383
|
|
171
384
|
locators
|
@@ -183,10 +396,17 @@ module AppiumFailureHelper
|
|
183
396
|
locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
|
184
397
|
end
|
185
398
|
|
186
|
-
# Fallback genérico (sempre adicionado)
|
187
399
|
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
188
400
|
|
189
401
|
locators
|
190
402
|
end
|
403
|
+
|
404
|
+
def self.setup_logger
|
405
|
+
@@logger = Logger.new(STDOUT)
|
406
|
+
@@logger.level = Logger::INFO
|
407
|
+
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
408
|
+
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
409
|
+
end
|
410
|
+
end
|
191
411
|
end
|
192
|
-
end
|
412
|
+
end
|