appium_failure_helper 0.3.1 → 0.4.1
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 +17 -26
- data/lib/appium_failure_helper/capture.rb +148 -36
- 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: 0c223153b4956f8098f653d255a4487bb3814bbd0ed8306cd4c01ccba478b4d9
|
4
|
+
data.tar.gz: 23ca5a6e67e1630473b37bd3ecaa1cab0b878ea35790e686bcd2eff6842462c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19483fead216b475eadac1f809e8bdd0ef2feafc46adfa41a396d18bb3572feb3a03b7768beacea1199046a4d7ffb982c2f9d8f9e4af13b1eee5fa035ff3a947
|
7
|
+
data.tar.gz: 2834dc179101c611fdce1262a20144f8684b86b3dea3515e3d2e4dccf983bbfa9e3e73b57fa1266d36c4a1b7deb4e93b35d7456ec75a7d9dbff5ec70cab542ab
|
data/README.md
CHANGED
@@ -1,43 +1,34 @@
|
|
1
1
|
# Appium Failure Helper
|
2
2
|
|
3
|
-
Este módulo Ruby foi projetado para
|
3
|
+
Este módulo Ruby foi projetado para ser uma ferramenta de diagnóstico inteligente para falhas em testes de automação mobile com **Appium**. Ele automatiza a captura de artefatos de depuração e a geração de sugestões de localizadores de elementos, eliminando a necessidade de usar o Appium Inspector.
|
4
4
|
|
5
5
|
## Funcionalidades Principais
|
6
6
|
|
7
|
-
* **
|
8
|
-
|
9
|
-
* **
|
10
|
-
|
11
|
-
* **
|
7
|
+
* **Análise de Falha Automatizada:** Captura o estado da aplicação no momento da falha.
|
8
|
+
* **Captura de Artefatos:** Salva um **screenshot** da tela e o **XML completo do `page_source`** em uma pasta dedicada por falha.
|
9
|
+
* **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.
|
10
|
+
* **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.
|
12
13
|
|
13
14
|
## Como Funciona
|
14
15
|
|
15
|
-
A lógica
|
16
|
-
|
17
|
-
1. **Criação de Diretório:** Garante que a pasta `screenshots/` exista para armazenar os artefatos.
|
18
|
-
|
19
|
-
2. **Captura de Screenshot e Page Source:** Utiliza o driver do Appium para obter o screenshot e o XML do `page_source`, salvando-os com um timestamp para evitar sobrescrever arquivos.
|
20
|
-
|
21
|
-
3. **Análise com Nokogiri:** O XML do `page_source` é parseado utilizando a gem `Nokogiri`.
|
22
|
-
|
23
|
-
4. **Processamento de Elementos:** O código itera sobre cada nó do XML (exceto o nó raiz 'hierarchy') e extrai atributos-chave como `resource-id`, `content-desc` e `text`.
|
16
|
+
A lógica do `AppiumFailureHelper` é ativada por um evento de falha em seu framework de testes (ex: Cucumber `After` hook). O método `handler_failure` executa as seguintes etapas:
|
24
17
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
6. **Saída Final:** O resultado é um arquivo `.yaml` contendo uma lista formatada de sugestões de locators no formato `["nome_sugerido", "xpath", "caminho_xpath"]`.
|
18
|
+
1. Cria um diretório de saída exclusivo.
|
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.
|
32
24
|
|
33
25
|
## Uso
|
34
26
|
|
35
|
-
Para usar este helper, integre-o ao seu framework de testes. Um exemplo comum é utilizá-lo em um hook `After` do Cucumber
|
27
|
+
Para usar este helper, integre-o ao seu framework de testes. Um exemplo comum é utilizá-lo em um hook `After` do Cucumber, passando o objeto de driver do Appium.
|
36
28
|
|
37
29
|
**`features/support/hooks.rb`**
|
38
|
-
|
39
|
-
|
40
|
-
require 'caminho/para/o/seu/modulo' # Ajuste o caminho
|
30
|
+
```ruby
|
31
|
+
require 'appium_failure_helper'
|
41
32
|
|
42
33
|
After do |scenario|
|
43
34
|
if scenario.failed?
|
@@ -1,80 +1,192 @@
|
|
1
1
|
require 'nokogiri'
|
2
2
|
require 'fileutils'
|
3
3
|
require 'base64'
|
4
|
+
require 'yaml'
|
4
5
|
|
5
6
|
module AppiumFailureHelper
|
6
7
|
class Capture
|
8
|
+
PREFIX = {
|
9
|
+
'android.widget.Button' => 'btn',
|
10
|
+
'android.widget.TextView' => 'txt',
|
11
|
+
'android.widget.ImageView' => 'img',
|
12
|
+
'android.widget.EditText' => 'input',
|
13
|
+
'android.widget.CheckBox' => 'chk',
|
14
|
+
'android.widget.RadioButton' => 'radio',
|
15
|
+
'android.widget.Switch' => 'switch',
|
16
|
+
'android.widget.ViewGroup' => 'group',
|
17
|
+
'android.widget.View' => 'view',
|
18
|
+
'android.widget.FrameLayout' => 'frame',
|
19
|
+
'android.widget.LinearLayout' => 'linear',
|
20
|
+
'android.widget.RelativeLayout' => 'relative',
|
21
|
+
'android.widget.ScrollView' => 'scroll',
|
22
|
+
'android.webkit.WebView' => 'web',
|
23
|
+
'android.widget.Spinner' => 'spin',
|
24
|
+
'XCUIElementTypeButton' => 'btn',
|
25
|
+
'XCUIElementTypeStaticText' => 'txt',
|
26
|
+
'XCUIElementTypeTextField' => 'input',
|
27
|
+
'XCUIElementTypeImage' => 'img',
|
28
|
+
'XCUIElementTypeSwitch' => 'switch',
|
29
|
+
'XCUIElementTypeScrollView' => 'scroll',
|
30
|
+
'XCUIElementTypeOther' => 'elm',
|
31
|
+
'XCUIElementTypeCell' => 'cell',
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
MAX_VALUE_LENGTH = 100
|
35
|
+
|
7
36
|
def self.handler_failure(driver)
|
8
37
|
begin
|
9
38
|
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
10
|
-
|
39
|
+
output_folder = "screenshots/failure_#{timestamp}"
|
11
40
|
|
12
|
-
FileUtils.mkdir_p(
|
41
|
+
FileUtils.mkdir_p(output_folder)
|
13
42
|
|
14
|
-
screenshot_path = "#{
|
43
|
+
screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
|
15
44
|
File.open(screenshot_path, 'wb') do |f|
|
16
45
|
f.write(Base64.decode64(driver.screenshot_as(:base64)))
|
17
46
|
end
|
18
47
|
puts "Screenshot saved to #{screenshot_path}"
|
19
48
|
|
20
49
|
page_source = driver.page_source
|
21
|
-
xml_path = "#{
|
50
|
+
xml_path = "#{output_folder}/page_source_#{timestamp}.xml"
|
22
51
|
File.write(xml_path, page_source)
|
23
52
|
|
24
53
|
doc = Nokogiri::XML(page_source)
|
25
54
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
'RadioButton' => 'radio',
|
33
|
-
'Switch' => 'switch',
|
34
|
-
}
|
35
|
-
|
36
|
-
line = doc.xpath('//*').map do |node|
|
55
|
+
platform = driver.capabilities['platformName']&.downcase || 'unknown'
|
56
|
+
|
57
|
+
seen_elements = {}
|
58
|
+
suggestions = []
|
59
|
+
|
60
|
+
doc.xpath('//*').each do |node|
|
37
61
|
next if node.name == 'hierarchy'
|
38
62
|
attrs = node.attributes.transform_values(&:value)
|
39
63
|
|
40
|
-
|
41
|
-
name = self.suggest_name(node.name, attrs, prefix)
|
42
|
-
xpath = self.xpath_generator(node.name, attrs)
|
64
|
+
unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
|
43
65
|
|
44
|
-
|
66
|
+
unless seen_elements[unique_key]
|
67
|
+
name = self.suggest_name(node.name, attrs)
|
68
|
+
locators = self.xpath_generator(node.name, attrs, platform)
|
69
|
+
|
70
|
+
suggestions << { name: name, locators: locators }
|
71
|
+
seen_elements[unique_key] = true
|
72
|
+
end
|
45
73
|
end
|
46
74
|
|
47
|
-
yaml_path = "#{
|
48
|
-
File.open(yaml_path, 'w')
|
75
|
+
yaml_path = "#{output_folder}/element_suggestions_#{timestamp}.yaml"
|
76
|
+
File.open(yaml_path, 'w') do |f|
|
77
|
+
f.write(YAML.dump(suggestions))
|
78
|
+
end
|
49
79
|
|
50
80
|
puts "Element suggestions saved to #{yaml_path}"
|
51
81
|
rescue => e
|
52
|
-
puts "Error capturing failure details: #{e.message}"
|
82
|
+
puts "Error capturing failure details: #{e.message}\n#{e.backtrace.join("\n")}"
|
53
83
|
end
|
54
84
|
end
|
55
85
|
|
56
86
|
private
|
87
|
+
|
88
|
+
def self.truncate(value)
|
89
|
+
return value unless value.is_a?(String)
|
90
|
+
value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
|
91
|
+
end
|
57
92
|
|
58
|
-
|
59
|
-
def self.suggest_name(tag, attrs, prefix)
|
93
|
+
def self.suggest_name(tag, attrs)
|
60
94
|
type = tag.split('.').last
|
61
|
-
pfx =
|
62
|
-
name = attrs['content-desc'] || attrs['text'] || attrs['resource-id'] || 'unknown' || type
|
63
|
-
name = name.strip.gsub(/[^0-9a-z]/, '').split.map(&:capitalize).join
|
95
|
+
pfx = PREFIX[tag] || PREFIX[type] || 'elm'
|
96
|
+
name = attrs['content-desc'] || attrs['text'] || attrs['resource-id'] || attrs['label'] || attrs['name'] || 'unknown' || type
|
97
|
+
name = truncate(name.strip.gsub(/[^0-9a-z]/, '').split.map(&:capitalize).join)
|
64
98
|
"#{pfx}#{name}"
|
65
99
|
end
|
66
100
|
|
67
|
-
def self.xpath_generator(tag, attrs)
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
elsif attrs['text'] && !attrs['text'].empty?
|
74
|
-
"//*[@text='#{attrs['text']}']"
|
101
|
+
def self.xpath_generator(tag, attrs, platform)
|
102
|
+
case platform
|
103
|
+
when 'android'
|
104
|
+
self.generate_android_xpaths(tag, attrs)
|
105
|
+
when 'ios'
|
106
|
+
self.generate_ios_xpaths(tag, attrs)
|
75
107
|
else
|
76
|
-
|
108
|
+
self.generate_unknown_xpaths(tag, attrs)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.generate_android_xpaths(tag, attrs)
|
113
|
+
locators = []
|
114
|
+
|
115
|
+
# Estratégia 1: Combinação de atributos
|
116
|
+
if attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['text'] && !attrs['text'].empty?
|
117
|
+
locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id='#{attrs['resource-id']}' and @text='#{self.truncate(attrs['text'])}']" }
|
118
|
+
elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
|
119
|
+
locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id='#{attrs['resource-id']}' and @content-desc='#{self.truncate(attrs['content-desc'])}']" }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Estratégia 2: ID único
|
123
|
+
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
124
|
+
locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id='#{attrs['resource-id']}']" }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Estratégia 3: starts-with para IDs dinâmicos
|
128
|
+
if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
|
129
|
+
id_part = attrs['resource-id'].split(':id/').last
|
130
|
+
locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, '#{id_part}')]" }
|
131
|
+
end
|
132
|
+
|
133
|
+
# Estratégia 4: Texto e content-desc como identificadores
|
134
|
+
if attrs['text'] && !attrs['text'].empty?
|
135
|
+
locators << { strategy: 'text', locator: "//#{tag}[@text='#{self.truncate(attrs['text'])}']" }
|
136
|
+
end
|
137
|
+
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
138
|
+
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc='#{self.truncate(attrs['content-desc'])}']" }
|
139
|
+
end
|
140
|
+
|
141
|
+
# Fallback genérico (sempre adicionado)
|
142
|
+
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
143
|
+
|
144
|
+
locators
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.generate_ios_xpaths(tag, attrs)
|
148
|
+
locators = []
|
149
|
+
|
150
|
+
# Estratégia 1: Combinação de atributos
|
151
|
+
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty? && attrs['label'] && !attrs['label'].empty?
|
152
|
+
locators << { strategy: 'accessibility_id_and_label', locator: "//#{tag}[@accessibility-id='#{attrs['accessibility-id']}' and @label='#{self.truncate(attrs['label'])}']" }
|
153
|
+
end
|
154
|
+
|
155
|
+
# Estratégia 2: ID único
|
156
|
+
if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
|
157
|
+
locators << { strategy: 'accessibility_id', locator: "//#{tag}[@accessibility-id='#{attrs['accessibility-id']}']" }
|
158
|
+
end
|
159
|
+
|
160
|
+
# Estratégia 3: label, name ou value
|
161
|
+
if attrs['label'] && !attrs['label'].empty?
|
162
|
+
locators << { strategy: 'label', locator: "//#{tag}[@label='#{self.truncate(attrs['label'])}']" }
|
163
|
+
end
|
164
|
+
if attrs['name'] && !attrs['name'].empty?
|
165
|
+
locators << { strategy: 'name', locator: "//#{tag}[@name='#{self.truncate(attrs['name'])}']" }
|
77
166
|
end
|
167
|
+
|
168
|
+
# Fallback genérico (sempre adicionado)
|
169
|
+
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
170
|
+
|
171
|
+
locators
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.generate_unknown_xpaths(tag, attrs)
|
175
|
+
locators = []
|
176
|
+
if attrs['resource-id'] && !attrs['resource-id'].empty?
|
177
|
+
locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id='#{attrs['resource-id']}']" }
|
178
|
+
end
|
179
|
+
if attrs['content-desc'] && !attrs['content-desc'].empty?
|
180
|
+
locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc='#{self.truncate(attrs['content-desc'])}']" }
|
181
|
+
end
|
182
|
+
if attrs['text'] && !attrs['text'].empty?
|
183
|
+
locators << { strategy: 'text', locator: "//#{tag}[@text='#{self.truncate(attrs['text'])}']" }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Fallback genérico (sempre adicionado)
|
187
|
+
locators << { strategy: 'generic_tag', locator: "//#{tag}" }
|
188
|
+
|
189
|
+
locators
|
78
190
|
end
|
79
191
|
end
|
80
192
|
end
|