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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7344cd53f36a48ed0689b608758d04724a1f2dc122197171e5b875bb08615fb
4
- data.tar.gz: acc66c6049fe82c85dc769591b7a81befda696cc5c4b02e8b8bd99ec60b643ac
3
+ metadata.gz: c0e272f2150ce88e42b3b414d0afeb9d4e2309311bd271d77bf912ac3967ad03
4
+ data.tar.gz: 982e97ea6c9a43da40ea9315f554bc734b076b20aaf68b61f1cef83bf3455349
5
5
  SHA512:
6
- metadata.gz: 44d7f03b734112353b8c5c7f0db157f125d54e7d9065471e5caba380c44d851f6a8eb757d410ec0f6280b412fc6b442eeb3b303143e98c0599b7d5adcf3d7792
7
- data.tar.gz: 48469708fd272ac232494b32bccbf1a99c4cc277a60cca1296eaf6d29128e55794a16987939a1f89fc3a9d8c2ec37ab9a56686a4edc80e4e4cd73de6948031cb
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
- 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.
3
+ [![Ruby](https://img.shields.io/badge/language-ruby-red.svg)](https://www.ruby-lang.org/)
4
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![Status](https://img.shields.io/badge/status-beta-yellow.svg)]()
4
6
 
5
- ## Funcionalidades Principais
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
- * **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.
9
+ ---
8
10
 
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.
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
- * **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.
30
+ ---
12
31
 
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.
32
+ ## Visão Geral
14
33
 
15
- * **Tratamento de Dados:** Trunca valores de atributos muito longos para evitar quebras no relatório e garante que não haja elementos duplicados.
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
- * **Sistema de Logging:** Usa a biblioteca padrão `Logger` do Ruby para fornecer feedback detalhado e limpo no console.
40
+ Todos os artefatos são salvos em uma pasta timestamped (formato `YYYY_MM_DD_HHMMSS`) dentro de `reports_failure/`.
18
41
 
19
- * **Múltiplos Relatórios:** Gera dois arquivos YAML: um **focado** no elemento que falhou e um **completo** com todos os elementos da tela.
42
+ ---
20
43
 
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.
44
+ ## Funcionalidades
22
45
 
23
- ## Como Funciona
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
- 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.
57
+ ---
26
58
 
27
- **`features/support/hooks.rb`**
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
- **Observação:** O nome da sua variável de driver pode variar. No exemplo, `appium_driver` deve ser o objeto de driver do seu teste.
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
- ## Artefatos Gerados
279
+ ## Segurança e Privacidade
43
280
 
44
- Após uma falha, os seguintes arquivos serão gerados na pasta `screenshots/`:
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
- * `screenshot_20231027_153045.png`
284
+ ---
47
285
 
48
- * `page_source_20231027_153045.xml`
286
+ ## Licença
49
287
 
50
- * `element_suggestions_20231027_153045.yaml`
288
+ MIT — veja o arquivo `LICENSE` para os termos.
51
289
 
52
- O arquivo `.yaml` é um recurso valioso para inspecionar os elementos da tela e atualizar seus localizadores de forma eficiente.
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 = Logger.new(STDOUT)
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
- # --- 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>
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
- <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>
151
+ # By.xpath: //..., By.id: "foo"
152
+ /By\.(xpath|id|css selector|cssSelector|name|class name|className):\s*['"]?(.+?)['"]?(?:\s|$)/i,
187
153
 
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>
154
+ # Generic "using <type>=<value>" or using <type>: '<value>'
155
+ /using\s+([a-zA-Z0-9_\-:]+)\s*[=:]\s*['"]?(.+?)['"]?(?:\s|$)/i,
194
156
 
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>
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
- <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>
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
- <script>
217
- document.addEventListener('DOMContentLoaded', () => {
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
- tabs.forEach(tab => {
222
- tab.addEventListener('click', () => {
223
- const target = tab.getAttribute('data-tab');
166
+ # Fallback to any "selector: '...'" occurence
167
+ /selector['"]?\s*[:=]?\s*['"](.+?)['"]/i
168
+ ]
224
169
 
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'));
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
- 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
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
- # --- Métodos de Suporte Existentes ---
243
-
244
- # ... (métodos setup_logger, extract_info_from_exception, find_similar_elements, etc.)
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?(failed_info[:selector_value]) ||
278
- attrs['text']&.include?(failed_info[:selector_value]) ||
279
- attrs['content-desc']&.include?(failed_info[:selector_value]))
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?(failed_info[:selector_value]) ||
282
- attrs['label']&.include?(failed_info[:selector_value]) ||
283
- attrs['name']&.include?(failed_info[:selector_value]))
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
- 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"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppiumFailureHelper
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.1"
5
5
  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.5.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-23 00:00:00.000000000 Z
11
+ date: 2025-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri