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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f76409f1b9c5f4bd448918dd57bcf691b317f2e9f8e49fdc077fb051b731c9c2
4
- data.tar.gz: c38273c8afae6609ca72a3a0fdea5713ca3d358f1d0ee50a562f9d9cff9d62bf
3
+ metadata.gz: 0c223153b4956f8098f653d255a4487bb3814bbd0ed8306cd4c01ccba478b4d9
4
+ data.tar.gz: 23ca5a6e67e1630473b37bd3ecaa1cab0b878ea35790e686bcd2eff6842462c2
5
5
  SHA512:
6
- metadata.gz: e7824f6860fb399ff2d735f410e779f1cc93582131e5ab5672eff5f85f8f0d5c8bfb3c434defa92de4aea24ec75714b3f7920cb7c500d7a48d81612b30fda052
7
- data.tar.gz: 7761f6376e1f9a2b284a6df260873af206ac550fa9374676eb1673e4fa2abf448deab8f4529e22d1c1b168c044d0486b548b1aba64e79f2e56199d7a183374a1
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 auxiliar na **análise de falhas em testes de automação Appium**. Ao ser invocado, ele captura o estado da aplicação no momento da falha e gera um conjunto de artefatos de diagnóstico, facilitando a identificação da causa raiz do problema e a sugestão de novos localizadores de elementos.
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
- * **Captura de Screenshot:** Salva uma imagem PNG da tela do dispositivo no momento da falha.
8
-
9
- * **Captura de Page Source:** Salva o XML do `page_source` completo, representando a hierarquia de elementos da tela.
10
-
11
- * **Geração de Sugestões de Elementos:** Analisa o `page_source` e gera um arquivo `.yaml` com sugestões de nomes e caminhos XPath para os elementos visíveis na tela.
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 central do módulo `AppiumFailureHelper` é acionada por um evento de falha no seu framework de testes (ex: Cucumber `After` hook). A função `handler_failure` executa as seguintes etapas:
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
- 5. **Geração de Nomes e XPath:**
26
-
27
- * `suggest_name`: Constrói um nome descritivo para cada elemento, utilizando prefixos comuns (`btn`, `txt`, `input`, etc.) e o valor dos atributos principais.
28
-
29
- * `xpath_generator`: Prioriza atributos mais confiáveis (`resource-id`, `content-desc`, `text`) para gerar um XPath robusto.
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
- folder_path = "screenshots"
39
+ output_folder = "screenshots/failure_#{timestamp}"
11
40
 
12
- FileUtils.mkdir_p(folder_path)
41
+ FileUtils.mkdir_p(output_folder)
13
42
 
14
- screenshot_path = "#{folder_path}/screenshot_#{timestamp}.png"
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 = "#{folder_path}/page_source_#{timestamp}.xml"
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
- prefix = {
27
- 'Button' => 'btn',
28
- 'TextView' => 'txt',
29
- 'ImageView' => 'img',
30
- 'EditText' => 'input',
31
- 'CheckBox' => 'chk',
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
- # Os métodos agora estão no escopo correto.
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
- "[\"#{name}\", \"xpath\", \"#{xpath}\"]"
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 = "#{folder_path}/element_suggestions_#{timestamp}.yaml"
48
- File.open(yaml_path, 'w') {|f| f.puts(line.compact.join("\n"))}
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
- # Mova os métodos para cá.
59
- def self.suggest_name(tag, attrs, prefix)
93
+ def self.suggest_name(tag, attrs)
60
94
  type = tag.split('.').last
61
- pfx = prefix[type] || 'elm'
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
- type = tag.split('.').last
69
- if attrs['resource-id'] && !attrs['resource-id'].empty?
70
- "//*[@resource-id='#{attrs['resource-id']}']"
71
- elsif attrs['content-desc'] && !attrs['content-desc'].empty?
72
- "//*[@content-desc='#{attrs['content-desc']}']"
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
- "//#{type}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppiumFailureHelper
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.1"
5
5
  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: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento