appium_failure_helper 0.3.1 → 0.4.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: f76409f1b9c5f4bd448918dd57bcf691b317f2e9f8e49fdc077fb051b731c9c2
4
- data.tar.gz: c38273c8afae6609ca72a3a0fdea5713ca3d358f1d0ee50a562f9d9cff9d62bf
3
+ metadata.gz: 995c0139fe92b684bf7133a42cc97c17620f59b6a827aa105d2360157c6c861f
4
+ data.tar.gz: cd712e634e60764c25d781f72caee2f6c4f2d7ee101e67c46a8eb021ac8cbd5b
5
5
  SHA512:
6
- metadata.gz: e7824f6860fb399ff2d735f410e779f1cc93582131e5ab5672eff5f85f8f0d5c8bfb3c434defa92de4aea24ec75714b3f7920cb7c500d7a48d81612b30fda052
7
- data.tar.gz: 7761f6376e1f9a2b284a6df260873af206ac550fa9374676eb1673e4fa2abf448deab8f4529e22d1c1b168c044d0486b548b1aba64e79f2e56199d7a183374a1
6
+ metadata.gz: d44e926ad1ae38a55f61b8dc8efa009385b91bbef5755339e08493fff6b3d0292f1524c51ead0f2753c5a480cb536cdf9abd8dc51ec9446bb126a2732ed2395b
7
+ data.tar.gz: 8c1855b1d74c7b0b574659b19e4dba1a0c35bac085c293a2033f536b6dfbb05765de513cedccbe4ad5780618eb2a43ca8e0278974f9f2efb09f31041ecc10b22
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,181 @@
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
+
7
34
  def self.handler_failure(driver)
8
35
  begin
9
36
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
10
- folder_path = "screenshots"
37
+ output_folder = "screenshots/failure_#{timestamp}"
11
38
 
12
- FileUtils.mkdir_p(folder_path)
39
+ FileUtils.mkdir_p(output_folder)
13
40
 
14
- screenshot_path = "#{folder_path}/screenshot_#{timestamp}.png"
41
+ screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
15
42
  File.open(screenshot_path, 'wb') do |f|
16
43
  f.write(Base64.decode64(driver.screenshot_as(:base64)))
17
44
  end
18
45
  puts "Screenshot saved to #{screenshot_path}"
19
46
 
20
47
  page_source = driver.page_source
21
- xml_path = "#{folder_path}/page_source_#{timestamp}.xml"
48
+ xml_path = "#{output_folder}/page_source_#{timestamp}.xml"
22
49
  File.write(xml_path, page_source)
23
50
 
24
51
  doc = Nokogiri::XML(page_source)
25
52
 
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|
37
- next if node.name == 'hierarchy'
38
- attrs = node.attributes.transform_values(&:value)
39
-
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)
43
-
44
- "[\"#{name}\", \"xpath\", \"#{xpath}\"]"
45
- end
53
+ platform = driver.capabilities['platformName']&.downcase || 'unknown'
54
+
55
+ yaml_path = "#{output_folder}/element_suggestions_#{timestamp}.yaml"
56
+ File.open(yaml_path, 'w') do |f|
57
+ suggestions = doc.xpath('//*').map do |node|
58
+ next if node.name == 'hierarchy'
59
+ attrs = node.attributes.transform_values(&:value)
60
+
61
+ name = self.suggest_name(node.name, attrs)
62
+ xpath = self.xpath_generator(node.name, attrs, platform)
46
63
 
47
- yaml_path = "#{folder_path}/element_suggestions_#{timestamp}.yaml"
48
- File.open(yaml_path, 'w') {|f| f.puts(line.compact.join("\n"))}
64
+ parent_node = node.parent
65
+ parent_xpath = parent_node ? parent_node.path : nil
66
+
67
+ {
68
+ name: name,
69
+ type: 'xpath',
70
+ locator: xpath,
71
+ parent_locator: parent_xpath
72
+ }
73
+ end.compact
74
+
75
+ f.write(YAML.dump(suggestions))
76
+ end
49
77
 
50
78
  puts "Element suggestions saved to #{yaml_path}"
51
79
  rescue => e
52
- puts "Error capturing failure details: #{e.message}"
80
+ puts "Error capturing failure details: #{e.message}\n#{e.backtrace.join("\n")}"
53
81
  end
54
82
  end
55
83
 
56
84
  private
57
-
58
- # Mova os métodos para cá.
59
- def self.suggest_name(tag, attrs, prefix)
85
+
86
+ def self.suggest_name(tag, attrs)
60
87
  type = tag.split('.').last
61
- pfx = prefix[type] || 'elm'
62
- name = attrs['content-desc'] || attrs['text'] || attrs['resource-id'] || 'unknown' || type
88
+ pfx = PREFIX[tag] || PREFIX[type] || 'elm'
89
+ name = attrs['content-desc'] || attrs['text'] || attrs['resource-id'] || attrs['label'] || attrs['name'] || 'unknown' || type
63
90
  name = name.strip.gsub(/[^0-9a-z]/, '').split.map(&:capitalize).join
64
91
  "#{pfx}#{name}"
65
92
  end
66
93
 
67
- def self.xpath_generator(tag, attrs)
68
- type = tag.split('.').last
94
+ def self.xpath_generator(tag, attrs, platform)
95
+ case platform
96
+ when 'android'
97
+ self.generate_android_xpath(tag, attrs)
98
+ when 'ios'
99
+ self.generate_ios_xpath(tag, attrs)
100
+ else
101
+ self.generate_unknown_xpath(tag, attrs)
102
+ end
103
+ end
104
+
105
+ def self.generate_android_xpath(tag, attrs)
106
+ strategies = [
107
+ # Estratégia 1: Combinação de resource-id e text/content-desc
108
+ -> {
109
+ if attrs['resource-id'] && !attrs['resource-id'].empty?
110
+ if attrs['text'] && !attrs['text'].empty?
111
+ return "//#{tag}[@resource-id='#{attrs['resource-id']}' and @text='#{attrs['text']}']"
112
+ elsif attrs['content-desc'] && !attrs['content-desc'].empty?
113
+ return "//#{tag}[@resource-id='#{attrs['resource-id']}' and @content-desc='#{attrs['content-desc']}']"
114
+ end
115
+ end
116
+ nil
117
+ },
118
+ # Estratégia 2: resource-id único
119
+ -> { "//#{tag}[@resource-id='#{attrs['resource-id']}']" if attrs['resource-id'] && !attrs['resource-id'].empty? },
120
+ # Estratégia 3: starts-with para resource-id dinâmico
121
+ -> {
122
+ if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
123
+ id_part = attrs['resource-id'].split(':id/').last
124
+ return "//#{tag}[starts-with(@resource-id, '#{id_part}')]"
125
+ end
126
+ nil
127
+ },
128
+ # Estratégia 4: text como identificador
129
+ -> { "//#{tag}[@text='#{attrs['text']}']" if attrs['text'] && !attrs['text'].empty? },
130
+ # Estratégia 5: content-desc como identificador
131
+ -> { "//#{tag}[@content-desc='#{attrs['content-desc']}']" if attrs['content-desc'] && !attrs['content-desc'].empty? },
132
+ # Estratégia 6: Fallback genérico
133
+ -> { "//#{tag}" }
134
+ ]
135
+
136
+ strategies.each do |strategy|
137
+ result = strategy.call
138
+ return result if result
139
+ end
140
+ end
141
+
142
+ def self.generate_ios_xpath(tag, attrs)
143
+ strategies = [
144
+ # Estratégia 1: Combinação de accessibility-id e label
145
+ -> {
146
+ if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
147
+ if attrs['label'] && !attrs['label'].empty?
148
+ return "//#{tag}[@accessibility-id='#{attrs['accessibility-id']}' and @label='#{attrs['label']}']"
149
+ end
150
+ end
151
+ nil
152
+ },
153
+ # Estratégia 2: accessibility-id como identificador
154
+ -> { "//#{tag}[@accessibility-id='#{attrs['accessibility-id']}']" if attrs['accessibility-id'] && !attrs['accessibility-id'].empty? },
155
+ # Estratégia 3: label como identificador
156
+ -> { "//#{tag}[@label='#{attrs['label']}']" if attrs['label'] && !attrs['label'].empty? },
157
+ # Estratégia 4: name como identificador
158
+ -> { "//#{tag}[@name='#{attrs['name']}']" if attrs['name'] && !attrs['name'].empty? },
159
+ # Estratégia 5: Fallback genérico
160
+ -> { "//#{tag}" }
161
+ ]
162
+
163
+ strategies.each do |strategy|
164
+ result = strategy.call
165
+ return result if result
166
+ end
167
+ end
168
+
169
+ def self.generate_unknown_xpath(tag, attrs)
69
170
  if attrs['resource-id'] && !attrs['resource-id'].empty?
70
- "//*[@resource-id='#{attrs['resource-id']}']"
171
+ "//#{tag}[@resource-id='#{attrs['resource-id']}']"
71
172
  elsif attrs['content-desc'] && !attrs['content-desc'].empty?
72
- "//*[@content-desc='#{attrs['content-desc']}']"
173
+ "//#{tag}[@content-desc='#{attrs['content-desc']}']"
73
174
  elsif attrs['text'] && !attrs['text'].empty?
74
- "//*[@text='#{attrs['text']}']"
175
+ "//#{tag}[@text='#{attrs['text']}']"
75
176
  else
76
- "//#{type}"
177
+ "//#{tag}"
77
178
  end
78
179
  end
79
180
  end
80
- end
181
+ 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.0"
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento