appium_failure_helper 0.5.0 → 0.6.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 +258 -20
- data/lib/appium_failure_helper/capture.rb +197 -143
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0e272f2150ce88e42b3b414d0afeb9d4e2309311bd271d77bf912ac3967ad03
|
4
|
+
data.tar.gz: 982e97ea6c9a43da40ea9315f554bc734b076b20aaf68b61f1cef83bf3455349
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d431703babb06ffde65176ec6b16233738790261efbee46376f2b6bb90f1fd42717505d4ab6c78b454bf61bc3e656c2f8e9e1ef3936ea8841b585ec152e51f3
|
7
|
+
data.tar.gz: d7df2cb2f3757ae8298ce7b8057e11ea66b0112ba38fb15e01e6e8bcb73b3dad26035bcfdeba8dc97e823bf608de787b2b47c3dceb28d0b988525812b4730ebd
|
data/README.md
CHANGED
@@ -1,32 +1,130 @@
|
|
1
1
|
# Appium Failure Helper
|
2
2
|
|
3
|
-
|
3
|
+
[](https://www.ruby-lang.org/)
|
4
|
+
[](LICENSE)
|
5
|
+
[]()
|
4
6
|
|
5
|
-
|
7
|
+
**Appium Failure Helper** é um módulo Ruby destinado a automatizar diagnóstico de falhas em testes de automação mobile com **Appium**. O objetivo é reduzir tempo de triagem, fornecer localizadores confiáveis e coletar artefatos de depuração sem depender do Appium Inspector.
|
6
8
|
|
7
|
-
|
9
|
+
---
|
8
10
|
|
9
|
-
|
11
|
+
## Sumário
|
12
|
+
- [Visão Geral](#visão-geral)
|
13
|
+
- [Funcionalidades](#funcionalidades)
|
14
|
+
- [Arquitetura e Fluxo](#arquitetura-e-fluxo)
|
15
|
+
- [Instalação](#instalação)
|
16
|
+
- [Configuração (opcional)](#configuração-opcional)
|
17
|
+
- [API Pública / Integração](#api-pública--integração)
|
18
|
+
- [Exemplos de Uso](#exemplos-de-uso)
|
19
|
+
- [Cucumber (hook After)](#cucumber-hook-after)
|
20
|
+
- [RSpec (after :each)](#rspec-after-each)
|
21
|
+
- [Formato dos Artefatos Gerados](#formato-dos-artefatos-gerados)
|
22
|
+
- [Lógica de Geração de XPaths (detalhada)](#lógica-de-geração-de-xpaths-detalhada)
|
23
|
+
- [Tratamento de Dados e Deduplicação](#tratamento-de-dados-e-deduplicação)
|
24
|
+
- [Relatório HTML Interativo](#relatório-html-interativo)
|
25
|
+
- [Logging e Observabilidade](#logging-e-observabilidade)
|
26
|
+
- [Testes e Qualidade](#testes-e-qualidade)
|
27
|
+
- [Roadmap e Contribuição](#roadmap-e-contribuição)
|
28
|
+
- [Licença](#licença)
|
10
29
|
|
11
|
-
|
30
|
+
---
|
12
31
|
|
13
|
-
|
32
|
+
## Visão Geral
|
14
33
|
|
15
|
-
|
34
|
+
No momento em que um teste falha, o módulo realiza, de forma atômica e thread-safe:
|
35
|
+
1. captura de screenshot,
|
36
|
+
2. extração do `page_source` completo (XML),
|
37
|
+
3. varredura da árvore de elementos para gerar localizadores sugeridos,
|
38
|
+
4. escrita de dois YAMLs (focado e completo) e um relatório HTML que agrega tudo.
|
16
39
|
|
17
|
-
|
40
|
+
Todos os artefatos são salvos em uma pasta timestamped (formato `YYYY_MM_DD_HHMMSS`) dentro de `reports_failure/`.
|
18
41
|
|
19
|
-
|
42
|
+
---
|
20
43
|
|
21
|
-
|
44
|
+
## Funcionalidades
|
22
45
|
|
23
|
-
|
46
|
+
- Captura automática de screenshot PNG.
|
47
|
+
- Export completo de `page_source` em XML.
|
48
|
+
- Geração de `failure_analysis_*.yaml` (focado no elemento que falhou).
|
49
|
+
- Geração de `all_elements_dump_*.yaml` (todos os elementos com localizadores sugeridos).
|
50
|
+
- Relatório HTML interativo que combine screenshot, XML formatado e lista de localizadores.
|
51
|
+
- Geração de XPaths otimizados para **Android** e **iOS**.
|
52
|
+
- Truncamento de atributos longos (configurável).
|
53
|
+
- Eliminação de elementos duplicados e normalização de atributos.
|
54
|
+
- Logging via `Logger` do Ruby (Níveis: DEBUG/INFO/WARN/ERROR).
|
55
|
+
- Configuração via bloco `configure` (opcional).
|
24
56
|
|
25
|
-
|
57
|
+
---
|
26
58
|
|
27
|
-
|
59
|
+
## Arquitetura e Fluxo
|
60
|
+
|
61
|
+
1. **Hook de Testes** (Cucumber/RSpec) → invoca `Capture.handler_failure(driver, exception)`
|
62
|
+
2. **Capture.handler_failure**:
|
63
|
+
- estabelece pasta de saída com timestamp;
|
64
|
+
- chama `driver.screenshot` (salva PNG);
|
65
|
+
- chama `driver.page_source` (salva XML);
|
66
|
+
- percorre XML e cria árvore de elementos;
|
67
|
+
- para cada elemento gera candidate XPaths aplicando regras por plataforma;
|
68
|
+
- grava `failure_analysis_*.yaml` (prioriza elemento indicado) e `all_elements_dump_*.yaml`;
|
69
|
+
- monta `report_*.html` agregando tudo.
|
70
|
+
3. Logs detalhados emitidos durante a execução.
|
71
|
+
|
72
|
+
---
|
73
|
+
|
74
|
+
## Instalação
|
75
|
+
|
76
|
+
**Como gem (exemplo):**
|
77
|
+
|
78
|
+
Adicione ao `Gemfile` do projeto:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
gem 'appium_failure_helper', '~> 0.1.0'
|
82
|
+
```
|
83
|
+
|
84
|
+
Depois:
|
85
|
+
|
86
|
+
```bash
|
87
|
+
bundle install
|
88
|
+
```
|
89
|
+
|
90
|
+
**Ou manual (para uso local):**
|
91
|
+
|
92
|
+
Coloque o diretório `appium_failure_helper/` dentro do `lib/` do projeto e faça:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
require_relative 'lib/appium_failure_helper'
|
96
|
+
```
|
97
|
+
|
98
|
+
---
|
99
|
+
|
100
|
+
## API Pública / Integração
|
101
|
+
|
102
|
+
### `AppiumFailureHelper::Capture`
|
28
103
|
|
29
104
|
```ruby
|
105
|
+
# handler_failure(driver, exception, options = {})
|
106
|
+
# - driver: objeto de sessão Appium (Selenium::WebDriver / Appium::Driver)
|
107
|
+
# - exception: exceção capturada no momento da falha
|
108
|
+
# - options: hash com overrides (ex: output_dir:)
|
109
|
+
AppiumFailureHelper::Capture.handler_failure(appium_driver, scenario.exception)
|
110
|
+
```
|
111
|
+
|
112
|
+
### Configuração global
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
AppiumFailureHelper.configure do |c|
|
116
|
+
# ver bloco de configuração acima
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
---
|
121
|
+
|
122
|
+
## Exemplos de Uso
|
123
|
+
|
124
|
+
### Cucumber (hook `After`)
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# features/support/hooks.rb
|
30
128
|
require 'appium_failure_helper'
|
31
129
|
|
32
130
|
After do |scenario|
|
@@ -34,19 +132,159 @@ After do |scenario|
|
|
34
132
|
AppiumFailureHelper::Capture.handler_failure(appium_driver, scenario.exception)
|
35
133
|
end
|
36
134
|
end
|
135
|
+
```
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
## Formato dos Artefatos Gerados
|
140
|
+
|
141
|
+
**Pasta:** `reports_failure/<TIMESTAMP>/`
|
37
142
|
|
143
|
+
Arquivos gerados (ex.: TIMESTAMP = `2025_09_23_173045`):
|
144
|
+
|
145
|
+
```
|
146
|
+
screenshot_2025_09_23_173045.png
|
147
|
+
page_source_2025_09_23_173045.xml
|
148
|
+
failure_analysis_2025_09_23_173045.yaml
|
149
|
+
all_elements_dump_2025_09_23_173045.yaml
|
150
|
+
report_2025_09_23_173045.html
|
151
|
+
```
|
152
|
+
|
153
|
+
### Exemplo (simplificado) de `failure_analysis_*.yaml`
|
154
|
+
|
155
|
+
```yaml
|
156
|
+
failed_element:
|
157
|
+
platform: android
|
158
|
+
summary:
|
159
|
+
class: android.widget.Button
|
160
|
+
resource_id: com.example:id/submit
|
161
|
+
text: "Enviar"
|
162
|
+
suggested_xpaths:
|
163
|
+
- "//android.widget.Button[@resource-id='com.example:id/submit']"
|
164
|
+
- "//android.widget.Button[contains(@text,'Enviar')]"
|
165
|
+
capture_metadata:
|
166
|
+
screenshot: screenshot_2025_09_23_173045.png
|
167
|
+
page_source: page_source_2025_09_23_173045.xml
|
168
|
+
timestamp: "2025-09-23T17:30:45Z"
|
169
|
+
tips: "Priorize resource-id; se ausente, use accessibility id (content-desc) e class+text como fallback."
|
170
|
+
```
|
171
|
+
|
172
|
+
### Exemplo (simplificado) de `all_elements_dump_*.yaml`
|
173
|
+
|
174
|
+
```yaml
|
175
|
+
elements:
|
176
|
+
- id_hash: "a1b2c3..."
|
177
|
+
class: "android.widget.EditText"
|
178
|
+
resource_id: "com.example:id/input_email"
|
179
|
+
text: "example@example.com"
|
180
|
+
truncated_attributes:
|
181
|
+
hint: "Digite seu e-mail..."
|
182
|
+
suggested_xpaths:
|
183
|
+
- "//*[@resource-id='com.example:id/input_email']"
|
184
|
+
- "//android.widget.EditText[contains(@hint,'Digite seu e-mail')]"
|
185
|
+
```
|
186
|
+
|
187
|
+
---
|
188
|
+
|
189
|
+
## Lógica de Geração de XPaths (detalhada)
|
190
|
+
|
191
|
+
**Princípios gerais**
|
192
|
+
1. Priorizar identificadores estáveis (resource-id no Android / accessibility id no iOS).
|
193
|
+
2. Evitar XPaths com `index` como primeira opção (usado apenas como último recurso).
|
194
|
+
3. Combinar atributos quando necessário para aumentar a especificidade e evitar colisões.
|
195
|
+
4. Normalizar espaços e truncar textos longos.
|
196
|
+
|
197
|
+
**Estratégias por plataforma (ordem de preferência)**
|
198
|
+
|
199
|
+
- **Android**
|
200
|
+
1. `resource-id` → `//*[@resource-id='com.pkg:id/id']`
|
201
|
+
2. `content-desc` / `contentDescription` (accessibility) → `//*[@content-desc='x']`
|
202
|
+
3. `class` + `text` → `//android.widget.TextView[@class='...' and contains(normalize-space(@text),'...')]`
|
203
|
+
4. `class` + raça de atributos (combinações: enabled, clickable, package)
|
204
|
+
5. fallback: `//android.widget.Button[position()=n]` (último recurso)
|
205
|
+
|
206
|
+
- **iOS**
|
207
|
+
1. `accessibility id` (nome accessibility) → `//*[@name='Submit']`
|
208
|
+
2. `label` / `value` → `//*[contains(@label,'...')]`
|
209
|
+
3. `type` + `label` → `//XCUIElementTypeButton[@label='OK']`
|
210
|
+
4. fallback: hierarquia / indices
|
211
|
+
|
212
|
+
**Exemplo de XPath combinado (alta especificidade):**
|
213
|
+
|
214
|
+
```xpath
|
215
|
+
//android.widget.Button[@resource-id='com.example:id/submit' and contains(normalize-space(@text),'Enviar') and @clickable='true']
|
38
216
|
```
|
39
217
|
|
40
|
-
|
218
|
+
---
|
219
|
+
|
220
|
+
## Tratamento de Dados e Deduplicação
|
221
|
+
|
222
|
+
- **Truncamento**: atributos com comprimento acima de `attr_truncate_length` são truncados com sufixo `...` para evitar poluição do YAML.
|
223
|
+
- **Hash único por elemento**: é gerado um hash (sha1) baseado em conjunto de atributos relevantes (class+resource-id+content-desc+text) para identificar duplicados.
|
224
|
+
- **Remoção de nulos**: atributos vazios ou nulos são omitidos nos YAMLs.
|
225
|
+
- **Ordenação**: elementos no `all_elements_dump` são ordenados por prioridade de localizador (resource-id primeiro).
|
226
|
+
|
227
|
+
---
|
228
|
+
|
229
|
+
## Relatório HTML Interativo
|
230
|
+
|
231
|
+
O HTML gerado possui:
|
232
|
+
- Visualização inline do `screenshot` (img tag),
|
233
|
+
- Painel colapsável com o `page_source` (XML formatado e collapsible),
|
234
|
+
- Lista navegável de elementos com seus `suggested_xpaths` (botões para copiar),
|
235
|
+
- Ancoragem que permite focalizar: ao clicar em um XPath, realça o fragmento correspondente no XML (se possível),
|
236
|
+
- Metadados e link rápido para os YAMLs.
|
237
|
+
|
238
|
+
**Observação:** o HTML é gerado de forma estática — para realces dinâmicos é usado JavaScript simples embutido (sem dependências externas).
|
239
|
+
|
240
|
+
---
|
241
|
+
|
242
|
+
## Logging e Observabilidade
|
243
|
+
|
244
|
+
- Usa `Logger` padrão do Ruby:
|
245
|
+
- `DEBUG` para detalhamento completo (padrão em modo dev).
|
246
|
+
- `INFO` para resumo das ações realizadas.
|
247
|
+
- `WARN/ERROR` para problemas durante captura/escrita.
|
248
|
+
- Exemplos de mensagens:
|
249
|
+
- `[INFO] Creating failure report folder: reports_failure/2025_09_23_173045`
|
250
|
+
- `[DEBUG] Captured 4123 elements from page_source`
|
251
|
+
- `[ERROR] Failed to write screenshot: Permission denied`
|
252
|
+
|
253
|
+
---
|
254
|
+
|
255
|
+
## Testes e Qualidade
|
256
|
+
|
257
|
+
- Estrutura de testes sugerida: RSpec + fixtures com dumps de `page_source` para validar a geração de XPaths.
|
258
|
+
- Testes unitários para: truncamento, hash de deduplicação, geração de strategies, output YAML válido.
|
259
|
+
- CI: incluir step que valide YAML/HTML gerados (lint) e execute testes RSpec.
|
260
|
+
|
261
|
+
---
|
262
|
+
|
263
|
+
## Roadmap e Contribuição
|
264
|
+
|
265
|
+
**Funcionalidades previstas**
|
266
|
+
- Suporte a mapeamento visual (overlay) para apontar elemento sobre screenshot.
|
267
|
+
- Export para outros formatos (JSON/CSV).
|
268
|
+
- Integração com ferramentas de observabilidade (Sentry, Datadog).
|
269
|
+
- Modo headless para gerar relatórios offline em pipelines.
|
270
|
+
|
271
|
+
**Como contribuir**
|
272
|
+
1. Fork no repositório.
|
273
|
+
2. Crie branch com feature/bugfix.
|
274
|
+
3. Abra PR com descrição técnica das mudanças e testes.
|
275
|
+
4. Mantenha o estilo Ruby (RuboCop) e documentação atualizada.
|
276
|
+
|
277
|
+
---
|
41
278
|
|
42
|
-
##
|
279
|
+
## Segurança e Privacidade
|
43
280
|
|
44
|
-
|
281
|
+
- Evite capturar dados sensíveis em ambientes com PII. Implementar filtro por regex para mascarar dados (ex.: emails/telefones) antes de salvar YAMLs.
|
282
|
+
- Recomendado: executar limpeza em ambientes de produção.
|
45
283
|
|
46
|
-
|
284
|
+
---
|
47
285
|
|
48
|
-
|
286
|
+
## Licença
|
49
287
|
|
50
|
-
|
288
|
+
MIT — veja o arquivo `LICENSE` para os termos.
|
51
289
|
|
52
|
-
|
290
|
+
---
|
@@ -39,7 +39,6 @@ module AppiumFailureHelper
|
|
39
39
|
begin
|
40
40
|
self.setup_logger unless @@logger
|
41
41
|
|
42
|
-
# Remove a pasta reports_failure ao iniciar uma nova execução
|
43
42
|
FileUtils.rm_rf("reports_failure")
|
44
43
|
@@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
|
45
44
|
|
@@ -49,7 +48,6 @@ module AppiumFailureHelper
|
|
49
48
|
FileUtils.mkdir_p(output_folder)
|
50
49
|
@@logger.info("Pasta de saída criada: #{output_folder}")
|
51
50
|
|
52
|
-
# Captura o Base64 e salva o PNG
|
53
51
|
screenshot_base64 = driver.screenshot_as(:base64)
|
54
52
|
screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
|
55
53
|
File.open(screenshot_path, 'wb') do |f|
|
@@ -67,7 +65,6 @@ module AppiumFailureHelper
|
|
67
65
|
|
68
66
|
failed_element_info = self.extract_info_from_exception(exception)
|
69
67
|
|
70
|
-
# --- Processamento de todos os elementos ---
|
71
68
|
seen_elements = {}
|
72
69
|
all_elements_suggestions = []
|
73
70
|
doc.xpath('//*').each do |node|
|
@@ -85,7 +82,6 @@ module AppiumFailureHelper
|
|
85
82
|
end
|
86
83
|
end
|
87
84
|
|
88
|
-
# --- Geração do Relatório FOCADO (1) ---
|
89
85
|
targeted_report = {
|
90
86
|
failed_element: failed_element_info,
|
91
87
|
similar_elements: [],
|
@@ -101,20 +97,17 @@ module AppiumFailureHelper
|
|
101
97
|
end
|
102
98
|
@@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
|
103
99
|
|
104
|
-
# --- Geração do Relatório COMPLETO (2) ---
|
105
100
|
full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
|
106
101
|
File.open(full_dump_yaml_path, 'w') do |f|
|
107
102
|
f.write(YAML.dump(all_elements_suggestions))
|
108
103
|
end
|
109
104
|
@@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
|
110
105
|
|
111
|
-
# --- Geração do Relatório HTML (3) ---
|
112
106
|
html_report_path = "#{output_folder}/report_#{timestamp}.html"
|
113
107
|
html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
|
114
108
|
File.write(html_report_path, html_content)
|
115
109
|
@@logger.info("Relatório HTML completo salvo em #{html_report_path}")
|
116
110
|
|
117
|
-
|
118
111
|
rescue => e
|
119
112
|
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
120
113
|
end
|
@@ -123,148 +116,89 @@ module AppiumFailureHelper
|
|
123
116
|
private
|
124
117
|
|
125
118
|
def self.setup_logger
|
126
|
-
@@logger
|
119
|
+
@@logger ||= Logger.new(STDOUT)
|
127
120
|
@@logger.level = Logger::INFO
|
128
121
|
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
129
122
|
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
130
123
|
end
|
131
124
|
end
|
125
|
+
|
126
|
+
def self.extract_info_from_exception(exception)
|
127
|
+
message = exception.to_s
|
128
|
+
info = {}
|
129
|
+
|
130
|
+
# normaliza tipos capturados para nomes previsíveis
|
131
|
+
normalize_type = lambda do |t|
|
132
|
+
return 'unknown' unless t
|
133
|
+
t = t.to_s.downcase.strip
|
134
|
+
t = t.gsub(/["']/, '')
|
135
|
+
case t
|
136
|
+
when 'cssselector', 'css selector' then 'css'
|
137
|
+
when 'classname', 'class name' then 'class name'
|
138
|
+
when 'accessibilityid', 'accessibility-id', 'accessibility id' then 'accessibility-id'
|
139
|
+
when 'resourceid', 'resource-id', 'resource id', 'id' then 'resource-id'
|
140
|
+
when 'contentdesc', 'content-desc', 'content desc' then 'content-desc'
|
141
|
+
else
|
142
|
+
t.gsub(/\s+/, '_').gsub(/[^a-z0-9_\-]/, '')
|
143
|
+
end
|
144
|
+
end
|
132
145
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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>
|
146
|
+
patterns = [
|
147
|
+
# ChromeDriver/Selenium JSON style:
|
148
|
+
# no such element: Unable to locate element: {"method":"xpath","selector":"//..."}
|
149
|
+
/no such element: Unable to locate element:\s*\{\s*["']?method["']?\s*:\s*["']?([^"'\}]+)["']?\s*,\s*["']?selector["']?\s*:\s*["']?([^"']+)["']?\s*\}/i,
|
181
150
|
|
182
|
-
|
183
|
-
|
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>
|
151
|
+
# By.xpath: //..., By.id: "foo"
|
152
|
+
/By\.(xpath|id|css selector|cssSelector|name|class name|className):\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
187
153
|
|
188
|
-
|
189
|
-
|
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>
|
154
|
+
# Generic "using <type>=<value>" or using <type>: '<value>'
|
155
|
+
/using\s+([a-zA-Z0-9_\-:]+)\s*[=:]\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
194
156
|
|
195
|
-
|
196
|
-
|
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>
|
157
|
+
# "An element with the selector '...' was not found"
|
158
|
+
/An element with (?:the )?selector ['"](.+?)['"] (?:could not be found|was not found|not found|not be located)/i,
|
203
159
|
|
204
|
-
|
205
|
-
|
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>
|
160
|
+
# "with the resource-id 'xyz'" or "with the accessibility-id 'abc'"
|
161
|
+
/with the (resource-id|accessibility[- ]?id|content-?desc|label|name)\s*[:=]?\s*['"](.+?)['"]/i,
|
215
162
|
|
216
|
-
|
217
|
-
|
218
|
-
const tabs = document.querySelectorAll('.tab-button');
|
219
|
-
const contents = document.querySelectorAll('.tab-content');
|
163
|
+
# "Unable to find element by: id 'xyz'"
|
164
|
+
/Unable to find element by:\s*([a-zA-Z0-9_\- ]+)\s*[:=]?\s*['"]?(.+?)['"]?(?:\s|$)/i,
|
220
165
|
|
221
|
-
|
222
|
-
|
223
|
-
|
166
|
+
# Fallback to any "selector: '...'" occurence
|
167
|
+
/selector['"]?\s*[:=]?\s*['"](.+?)['"]/i
|
168
|
+
]
|
224
169
|
|
225
|
-
|
226
|
-
|
170
|
+
patterns.each do |pattern|
|
171
|
+
if (m = message.match(pattern))
|
172
|
+
caps = m.captures.compact
|
173
|
+
if caps.length >= 2
|
174
|
+
raw_type = caps[0].to_s.strip
|
175
|
+
raw_value = caps[1].to_s.strip
|
176
|
+
info[:selector_type] = normalize_type.call(raw_type)
|
177
|
+
info[:selector_value] = raw_value.gsub(/\A['"]|['"]\z/, '')
|
178
|
+
else
|
179
|
+
info[:selector_type] = 'unknown'
|
180
|
+
info[:selector_value] = caps[0].to_s.strip.gsub(/\A['"]|['"]\z/, '')
|
181
|
+
end
|
182
|
+
info[:raw_message] = message[0, 1000]
|
183
|
+
return info
|
184
|
+
end
|
185
|
+
end
|
227
186
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
|
235
|
-
});
|
236
|
-
</script>
|
237
|
-
</body>
|
238
|
-
</html>
|
239
|
-
HTML_REPORT
|
187
|
+
# tentativa extra: By.<tipo>:<valor> em qualquer lugar da mensagem
|
188
|
+
if (m = message.match(/By\.([a-zA-Z0-9_\- ]+):\s*['"]?(.+?)['"]?/i))
|
189
|
+
info[:selector_type] = normalize_type.call(m[1])
|
190
|
+
info[:selector_value] = m[2].to_s.strip
|
191
|
+
info[:raw_message] = message[0,1000]
|
192
|
+
return info
|
240
193
|
end
|
241
194
|
|
242
|
-
#
|
243
|
-
|
244
|
-
|
195
|
+
# fallback final: retorna a mensagem inteira recortada (útil para debug)
|
196
|
+
info[:selector_type] = 'unknown'
|
197
|
+
info[:selector_value] = message.strip[0, 500]
|
198
|
+
info[:raw_message] = message[0,1000]
|
199
|
+
info
|
200
|
+
end
|
245
201
|
|
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
202
|
|
269
203
|
def self.find_similar_elements(doc, failed_info, platform)
|
270
204
|
similar_elements = []
|
@@ -272,15 +206,17 @@ module AppiumFailureHelper
|
|
272
206
|
next if node.name == 'hierarchy'
|
273
207
|
attrs = node.attributes.transform_values(&:value)
|
274
208
|
|
209
|
+
selector_value = failed_info[:selector_value].to_s.downcase.strip
|
210
|
+
|
275
211
|
is_similar = case platform
|
276
212
|
when 'android'
|
277
|
-
(attrs['resource-id']&.include?(
|
278
|
-
attrs['text']&.include?(
|
279
|
-
attrs['content-desc']&.include?(
|
213
|
+
(attrs['resource-id']&.downcase&.include?(selector_value) ||
|
214
|
+
attrs['text']&.downcase&.include?(selector_value) ||
|
215
|
+
attrs['content-desc']&.downcase&.include?(selector_value))
|
280
216
|
when 'ios'
|
281
|
-
(attrs['accessibility-id']&.include?(
|
282
|
-
attrs['label']&.include?(
|
283
|
-
attrs['name']&.include?(
|
217
|
+
(attrs['accessibility-id']&.downcase&.include?(selector_value) ||
|
218
|
+
attrs['label']&.downcase&.include?(selector_value) ||
|
219
|
+
attrs['name']&.downcase&.include?(selector_value))
|
284
220
|
else
|
285
221
|
false
|
286
222
|
end
|
@@ -400,13 +336,131 @@ module AppiumFailureHelper
|
|
400
336
|
|
401
337
|
locators
|
402
338
|
end
|
339
|
+
|
340
|
+
def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
|
341
|
+
|
342
|
+
locators_html = lambda do |locators|
|
343
|
+
locators.map do |loc|
|
344
|
+
"<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>"
|
345
|
+
end.join
|
346
|
+
end
|
403
347
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
348
|
+
all_elements_html = lambda do |elements|
|
349
|
+
elements.map do |el|
|
350
|
+
"<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{el[:name]}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>"
|
351
|
+
end.join
|
352
|
+
end
|
353
|
+
|
354
|
+
failed_info = targeted_report[:failed_element]
|
355
|
+
similar_elements = targeted_report[:similar_elements]
|
356
|
+
|
357
|
+
similar_elements_content = similar_elements.empty? ? "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada. O elemento pode ter sido removido.</p>" : 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
|
358
|
+
|
359
|
+
failed_info_content = if failed_info && failed_info[:selector_value]
|
360
|
+
"<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'>#{failed_info[:selector_type]}</span></p><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'>#{failed_info[:selector_value]}</span></p>"
|
361
|
+
else
|
362
|
+
"<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
|
409
363
|
end
|
364
|
+
|
365
|
+
# Template HTML usando um heredoc
|
366
|
+
<<~HTML_REPORT
|
367
|
+
<!DOCTYPE html>
|
368
|
+
<html lang="pt-BR">
|
369
|
+
<head>
|
370
|
+
<meta charset="UTF-8">
|
371
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
372
|
+
<title>Relatório de Falha Appium - #{timestamp}</title>
|
373
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
374
|
+
<style>
|
375
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
|
376
|
+
.tab-content { display: none; }
|
377
|
+
.tab-content.active { display: block; }
|
378
|
+
.tab-button.active { background-color: #4f46e5; color: white; }
|
379
|
+
.tab-button:not(.active):hover { background-color: #e0e7ff; }
|
380
|
+
</style>
|
381
|
+
</head>
|
382
|
+
<body class="bg-gray-50 p-8">
|
383
|
+
<div class="max-w-7xl mx-auto">
|
384
|
+
<header class="mb-8 pb-4 border-b border-gray-300">
|
385
|
+
<h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
|
386
|
+
<p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
|
387
|
+
</header>
|
388
|
+
|
389
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
390
|
+
<!-- Coluna de Screenshots e Falha -->
|
391
|
+
<div class="lg:col-span-1">
|
392
|
+
<div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
|
393
|
+
<h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
|
394
|
+
#{failed_info_content}
|
395
|
+
</div>
|
396
|
+
|
397
|
+
<div class="bg-white p-4 rounded-lg shadow-xl">
|
398
|
+
<h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
|
399
|
+
<img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
|
400
|
+
</div>
|
401
|
+
</div>
|
402
|
+
|
403
|
+
<!-- Coluna de Relatórios e Sugestões -->
|
404
|
+
<div class="lg:col-span-2">
|
405
|
+
<div class="bg-white rounded-lg shadow-xl">
|
406
|
+
<!-- Abas de Navegação -->
|
407
|
+
<div class="flex border-b border-gray-200">
|
408
|
+
<button class="tab-button active px-4 py-3 text-sm font-medium rounded-tl-lg" data-tab="similar">Sugestões de Reparo (#{similar_elements.size})</button>
|
409
|
+
<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>
|
410
|
+
</div>
|
411
|
+
|
412
|
+
<!-- Conteúdo das Abas -->
|
413
|
+
<div class="p-6">
|
414
|
+
<!-- Aba Sugestões de Reparo -->
|
415
|
+
<div id="similar" class="tab-content active">
|
416
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
|
417
|
+
<div class="space-y-4">
|
418
|
+
#{similar_elements_content}
|
419
|
+
</div>
|
420
|
+
</div>
|
421
|
+
|
422
|
+
<!-- Aba Dump Completo -->
|
423
|
+
<div id="all" class="tab-content">
|
424
|
+
<h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
|
425
|
+
<div class="max-h-[600px] overflow-y-auto space-y-2">
|
426
|
+
#{all_elements_html.call(all_suggestions)}
|
427
|
+
</div>
|
428
|
+
</div>
|
429
|
+
</div>
|
430
|
+
</div>
|
431
|
+
</div>
|
432
|
+
</div>
|
433
|
+
</div>
|
434
|
+
|
435
|
+
<script>
|
436
|
+
document.addEventListener('DOMContentLoaded', () => {
|
437
|
+
const tabs = document.querySelectorAll('.tab-button');
|
438
|
+
const contents = document.querySelectorAll('.tab-content');
|
439
|
+
|
440
|
+
tabs.forEach(tab => {
|
441
|
+
tab.addEventListener('click', () => {
|
442
|
+
const target = tab.getAttribute('data-tab');
|
443
|
+
|
444
|
+
tabs.forEach(t => {
|
445
|
+
t.classList.remove('active', 'text-white', 'bg-indigo-600');
|
446
|
+
t.classList.add('text-gray-600');
|
447
|
+
});
|
448
|
+
contents.forEach(c => c.classList.remove('active'));
|
449
|
+
|
450
|
+
tab.classList.add('active', 'text-white', 'bg-indigo-600');
|
451
|
+
tab.classList.remove('text-gray-600');
|
452
|
+
document.getElementById(target).classList.add('active');
|
453
|
+
});
|
454
|
+
});
|
455
|
+
|
456
|
+
// Set initial active state for styling consistency
|
457
|
+
const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
|
458
|
+
activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
|
459
|
+
});
|
460
|
+
</script>
|
461
|
+
</body>
|
462
|
+
</html>
|
463
|
+
HTML_REPORT
|
410
464
|
end
|
411
465
|
end
|
412
466
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: appium_failure_helper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Nascimento
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-09-
|
11
|
+
date: 2025-09-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|