appium_failure_helper 0.4.2 → 0.6.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: c15543514bd288d39e5dd2d8d5d07b23aa9c21b78704b99c8e5eeca7400b08ed
4
- data.tar.gz: c41bfaac49d288ca60d99eb6708b6e4eb71074494f0ee615251ccf429920dca7
3
+ metadata.gz: cb96de86c564a4b997d4d1a4320e831e3d4f28a3f8d7e97623b347d855588303
4
+ data.tar.gz: 2462b34e6f3c01ea63a8a68d9c1814fd4138dc2c0dbcdbef0aecb8d6ba82fece
5
5
  SHA512:
6
- metadata.gz: 16773e8d4f28fdf2e453e80e739641756821a0efd9bb2c9df7917e67791b469b56e066a132992d38d361e68c9a66e7030fbae74fa7b9ae37253e620890ec8ac3
7
- data.tar.gz: 73dff6f6a7045ba2749e6aa3157cf141b9c5f8bb315de50ff9820cee1dde3e8b83c485fcfd8e19bd6260c4f77bae629aea62903dea2120240cb0e95b050075c6
6
+ metadata.gz: ac755fa2ec04c3038c27c9ded178be9b6499de3c7a9cc70c96a5dfebd2fd07d6304eb236720cc4e4dfb1f31807b731c50512973af875596925eef72a9290f1b0
7
+ data.tar.gz: 410501dec6ffa017effc75796cafb04b83a371bcd6f4b5b466b60f70339193b64f743112870e88cade771ed0ef70f4b0626fd4a5fc845173225d7cb4952052a5
data/README.md CHANGED
@@ -1,53 +1,290 @@
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
- * **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.
9
+ ---
13
10
 
14
- ## Como Funciona
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)
15
29
 
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:
30
+ ---
17
31
 
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
+ ## Visão Geral
24
33
 
25
- ## Uso
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.
26
39
 
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.
40
+ Todos os artefatos são salvos em uma pasta timestamped (formato `YYYY_MM_DD_HHMMSS`) dentro de `reports_failure/`.
41
+
42
+ ---
43
+
44
+ ## Funcionalidades
45
+
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).
56
+
57
+ ---
58
+
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:
28
79
 
29
- **`features/support/hooks.rb`**
30
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`
103
+
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
31
128
  require 'appium_failure_helper'
32
129
 
33
130
  After do |scenario|
34
131
  if scenario.failed?
35
- AppiumFailureHelper::Capture.handler_failure(appium_driver)
132
+ AppiumFailureHelper::Capture.handler_failure(appium_driver, scenario.exception)
36
133
  end
37
134
  end
135
+ ```
136
+
137
+ ---
38
138
 
139
+ ## Formato dos Artefatos Gerados
140
+
141
+ **Pasta:** `reports_failure/<TIMESTAMP>/`
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']
39
216
  ```
40
217
 
41
- **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
+ ---
42
278
 
43
- ## Artefatos Gerados
279
+ ## Segurança e Privacidade
44
280
 
45
- 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.
46
283
 
47
- * `screenshot_20231027_153045.png`
284
+ ---
48
285
 
49
- * `page_source_20231027_153045.xml`
286
+ ## Licença
50
287
 
51
- * `element_suggestions_20231027_153045.yaml`
288
+ MIT — veja o arquivo `LICENSE` para os termos.
52
289
 
53
- O arquivo `.yaml` é um recurso valioso para inspecionar os elementos da tela e atualizar seus localizadores de forma eficiente.
290
+ ---
@@ -2,6 +2,7 @@ require 'nokogiri'
2
2
  require 'fileutils'
3
3
  require 'base64'
4
4
  require 'yaml'
5
+ require 'logger'
5
6
 
6
7
  module AppiumFailureHelper
7
8
  class Capture
@@ -32,59 +33,158 @@ module AppiumFailureHelper
32
33
  }.freeze
33
34
 
34
35
  MAX_VALUE_LENGTH = 100
36
+ @@logger = nil
35
37
 
36
- def self.handler_failure(driver)
38
+ def self.handler_failure(driver, exception)
37
39
  begin
40
+ self.setup_logger unless @@logger
41
+
42
+ # Remove a pasta reports_failure ao iniciar uma nova execução
43
+ FileUtils.rm_rf("reports_failure")
44
+ @@logger.info("Pasta 'reports_failure' removida para uma nova execução.")
45
+
38
46
  timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
39
- output_folder = "screenshots/failure_#{timestamp}"
47
+ output_folder = "reports_failure/failure_#{timestamp}"
40
48
 
41
49
  FileUtils.mkdir_p(output_folder)
50
+ @@logger.info("Pasta de saída criada: #{output_folder}")
42
51
 
52
+ # Captura o Base64 e salva o PNG
53
+ screenshot_base64 = driver.screenshot_as(:base64)
43
54
  screenshot_path = "#{output_folder}/screenshot_#{timestamp}.png"
44
55
  File.open(screenshot_path, 'wb') do |f|
45
- f.write(Base64.decode64(driver.screenshot_as(:base64)))
56
+ f.write(Base64.decode64(screenshot_base64))
46
57
  end
47
- puts "Screenshot saved to #{screenshot_path}"
58
+ @@logger.info("Screenshot salvo em #{screenshot_path}")
48
59
 
49
60
  page_source = driver.page_source
50
61
  xml_path = "#{output_folder}/page_source_#{timestamp}.xml"
51
62
  File.write(xml_path, page_source)
63
+ @@logger.info("Page source salvo em #{xml_path}")
52
64
 
53
65
  doc = Nokogiri::XML(page_source)
54
-
55
66
  platform = driver.capabilities['platformName']&.downcase || 'unknown'
56
67
 
57
- seen_elements = {}
58
- suggestions = []
68
+ failed_element_info = self.extract_info_from_exception(exception)
59
69
 
70
+ # --- Processamento de todos os elementos ---
71
+ seen_elements = {}
72
+ all_elements_suggestions = []
60
73
  doc.xpath('//*').each do |node|
61
74
  next if node.name == 'hierarchy'
62
75
  attrs = node.attributes.transform_values(&:value)
63
76
 
64
- unique_key = "#{node.name}|#{attrs['resource-id']}|#{attrs['content-desc']}|#{attrs['text']}"
77
+ unique_key = "#{node.name}|#{attrs['resource-id'].to_s}|#{attrs['content-desc'].to_s}|#{attrs['text'].to_s}"
65
78
 
66
79
  unless seen_elements[unique_key]
67
80
  name = self.suggest_name(node.name, attrs)
68
81
  locators = self.xpath_generator(node.name, attrs, platform)
69
82
 
70
- suggestions << { name: name, locators: locators }
83
+ all_elements_suggestions << { name: name, locators: locators }
71
84
  seen_elements[unique_key] = true
72
85
  end
73
86
  end
74
87
 
75
- yaml_path = "#{output_folder}/element_suggestions_#{timestamp}.yaml"
76
- File.open(yaml_path, 'w') do |f|
77
- f.write(YAML.dump(suggestions))
88
+ # --- Geração do Relatório FOCADO (1) ---
89
+ targeted_report = {
90
+ failed_element: failed_element_info,
91
+ similar_elements: [],
92
+ }
93
+
94
+ if failed_element_info && failed_element_info[:selector_value]
95
+ targeted_report[:similar_elements] = self.find_similar_elements(doc, failed_element_info, platform)
96
+ end
97
+
98
+ targeted_yaml_path = "#{output_folder}/failure_analysis_#{timestamp}.yaml"
99
+ File.open(targeted_yaml_path, 'w') do |f|
100
+ f.write(YAML.dump(targeted_report))
101
+ end
102
+ @@logger.info("Análise direcionada salva em #{targeted_yaml_path}")
103
+
104
+ # --- Geração do Relatório COMPLETO (2) ---
105
+ full_dump_yaml_path = "#{output_folder}/all_elements_dump_#{timestamp}.yaml"
106
+ File.open(full_dump_yaml_path, 'w') do |f|
107
+ f.write(YAML.dump(all_elements_suggestions))
78
108
  end
109
+ @@logger.info("Dump completo da página salvo em #{full_dump_yaml_path}")
110
+
111
+ # --- Geração do Relatório HTML (3) ---
112
+ html_report_path = "#{output_folder}/report_#{timestamp}.html"
113
+ html_content = self.generate_html_report(targeted_report, all_elements_suggestions, screenshot_base64, platform, timestamp)
114
+ File.write(html_report_path, html_content)
115
+ @@logger.info("Relatório HTML completo salvo em #{html_report_path}")
79
116
 
80
- puts "Element suggestions saved to #{yaml_path}"
81
117
  rescue => e
82
- puts "Error capturing failure details: #{e.message}\n#{e.backtrace.join("\n")}"
118
+ @@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
83
119
  end
84
120
  end
85
121
 
86
122
  private
87
123
 
124
+ def self.setup_logger
125
+ @@logger = Logger.new(STDOUT)
126
+ @@logger.level = Logger::INFO
127
+ @@logger.formatter = proc do |severity, datetime, progname, msg|
128
+ "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
129
+ end
130
+ end
131
+
132
+ def self.extract_info_from_exception(exception)
133
+ message = exception.message
134
+ info = {}
135
+
136
+ # Corrigido: Usando múltiplos padrões para extração robusta (resolvendo o problema do YAML vazio)
137
+ patterns = [
138
+ /(?:could not be found|cannot find element) using (.+)=['"]?(.+)['"]?/i,
139
+ /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
140
+ /(?:An element with the selector |element with the selector |selector |element with the |element identified by )(.+?) (?:could not be found|was not found|not found|not be located)/i,
141
+ /(?:with the resource-id|with the accessibility-id) ['"](.+?)['"]/i
142
+ ]
143
+
144
+ patterns.each do |pattern|
145
+ match = message.match(pattern)
146
+ if match
147
+ selector_value = match.captures.last.strip
148
+ selector_type = match[1]&.strip || 'Unknown'
149
+
150
+ info[:selector_type] = selector_type
151
+ info[:selector_value] = selector_value.gsub(/['"]/, '')
152
+ return info
153
+ end
154
+ end
155
+ info
156
+ end
157
+
158
+ def self.find_similar_elements(doc, failed_info, platform)
159
+ similar_elements = []
160
+ doc.xpath('//*').each do |node|
161
+ next if node.name == 'hierarchy'
162
+ attrs = node.attributes.transform_values(&:value)
163
+
164
+ # Lógica aprimorada para comparação insensível a maiúsculas/minúsculas
165
+ selector_value = failed_info[:selector_value].to_s.downcase
166
+ is_similar = case platform
167
+ when 'android'
168
+ (attrs['resource-id']&.downcase&.include?(selector_value) ||
169
+ attrs['text']&.downcase&.include?(selector_value) ||
170
+ attrs['content-desc']&.downcase&.include?(selector_value))
171
+ when 'ios'
172
+ (attrs['accessibility-id']&.downcase&.include?(selector_value) ||
173
+ attrs['label']&.downcase&.include?(selector_value) ||
174
+ attrs['name']&.downcase&.include?(selector_value))
175
+ else
176
+ false
177
+ end
178
+
179
+ if is_similar
180
+ name = self.suggest_name(node.name, attrs)
181
+ locators = self.xpath_generator(node.name, attrs, platform)
182
+ similar_elements << { name: name, locators: locators }
183
+ end
184
+ end
185
+ similar_elements
186
+ end
187
+
88
188
  def self.truncate(value)
89
189
  return value unless value.is_a?(String)
90
190
  value.size > MAX_VALUE_LENGTH ? "#{value[0...MAX_VALUE_LENGTH]}..." : value
@@ -93,9 +193,22 @@ module AppiumFailureHelper
93
193
  def self.suggest_name(tag, attrs)
94
194
  type = tag.split('.').last
95
195
  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)
98
- "#{pfx}#{name}"
196
+ name_base = nil
197
+
198
+ ['content-desc', 'text', 'resource-id', 'label', 'name'].each do |attr_key|
199
+ value = attrs[attr_key]
200
+ if value.is_a?(String) && !value.empty?
201
+ name_base = value
202
+ break
203
+ end
204
+ end
205
+
206
+ name_base ||= type
207
+
208
+ truncated_name = truncate(name_base)
209
+ sanitized_name = truncated_name.gsub(/[^a-zA-Z0-9\s]/, ' ').split.map(&:capitalize).join
210
+
211
+ "#{pfx}#{sanitized_name}"
99
212
  end
100
213
 
101
214
  def self.xpath_generator(tag, attrs, platform)
@@ -112,25 +225,21 @@ module AppiumFailureHelper
112
225
  def self.generate_android_xpaths(tag, attrs)
113
226
  locators = []
114
227
 
115
- # Estratégia 1: Combinação de atributos
116
228
  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'])}\"]" }
229
+ locators << { strategy: 'resource_id_and_text', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @text=\"#{self.truncate(attrs['text'])}\"]" }
118
230
  elsif attrs['resource-id'] && !attrs['resource-id'].empty? && attrs['content-desc'] && !attrs['content-desc'].empty?
119
231
  locators << { strategy: 'resource_id_and_content_desc', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\" and @content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
120
232
  end
121
233
 
122
- # Estratégia 2: ID único
123
234
  if attrs['resource-id'] && !attrs['resource-id'].empty?
124
235
  locators << { strategy: 'resource_id', locator: "//#{tag}[@resource-id=\"#{attrs['resource-id']}\"]" }
125
236
  end
126
237
 
127
- # Estratégia 3: starts-with para IDs dinâmicos
128
238
  if attrs['resource-id'] && attrs['resource-id'].include?(':id/')
129
239
  id_part = attrs['resource-id'].split(':id/').last
130
240
  locators << { strategy: 'starts_with_resource_id', locator: "//#{tag}[starts-with(@resource-id, \"#{id_part}\")]" }
131
241
  end
132
242
 
133
- # Estratégia 4: Texto e content-desc como identificadores
134
243
  if attrs['text'] && !attrs['text'].empty?
135
244
  locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
136
245
  end
@@ -138,7 +247,6 @@ module AppiumFailureHelper
138
247
  locators << { strategy: 'content_desc', locator: "//#{tag}[@content-desc=\"#{self.truncate(attrs['content-desc'])}\"]" }
139
248
  end
140
249
 
141
- # Fallback genérico (sempre adicionado)
142
250
  locators << { strategy: 'generic_tag', locator: "//#{tag}" }
143
251
 
144
252
  locators
@@ -147,17 +255,14 @@ module AppiumFailureHelper
147
255
  def self.generate_ios_xpaths(tag, attrs)
148
256
  locators = []
149
257
 
150
- # Estratégia 1: Combinação de atributos
151
258
  if attrs['accessibility-id'] && !attrs['accessibility-id'].empty? && attrs['label'] && !attrs['label'].empty?
152
259
  locators << { strategy: 'accessibility_id_and_label', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\" and @label=\"#{self.truncate(attrs['label'])}\"]" }
153
260
  end
154
261
 
155
- # Estratégia 2: ID único
156
262
  if attrs['accessibility-id'] && !attrs['accessibility-id'].empty?
157
263
  locators << { strategy: 'accessibility_id', locator: "//#{tag}[@accessibility-id=\"#{attrs['accessibility-id']}\"]" }
158
264
  end
159
265
 
160
- # Estratégia 3: label, name ou value
161
266
  if attrs['label'] && !attrs['label'].empty?
162
267
  locators << { strategy: 'label', locator: "//#{tag}[@label=\"#{self.truncate(attrs['label'])}\"]" }
163
268
  end
@@ -165,7 +270,6 @@ module AppiumFailureHelper
165
270
  locators << { strategy: 'name', locator: "//#{tag}[@name=\"#{self.truncate(attrs['name'])}\"]" }
166
271
  end
167
272
 
168
- # Fallback genérico (sempre adicionado)
169
273
  locators << { strategy: 'generic_tag', locator: "//#{tag}" }
170
274
 
171
275
  locators
@@ -183,10 +287,135 @@ module AppiumFailureHelper
183
287
  locators << { strategy: 'text', locator: "//#{tag}[@text=\"#{self.truncate(attrs['text'])}\"]" }
184
288
  end
185
289
 
186
- # Fallback genérico (sempre adicionado)
187
290
  locators << { strategy: 'generic_tag', locator: "//#{tag}" }
188
291
 
189
292
  locators
190
293
  end
294
+
295
+ def self.generate_html_report(targeted_report, all_suggestions, screenshot_base64, platform, timestamp)
296
+
297
+ locators_html = lambda do |locators|
298
+ locators.map do |loc|
299
+ "<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>"
300
+ end.join
301
+ end
302
+
303
+ all_elements_html = lambda do |elements|
304
+ elements.map do |el|
305
+ "<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>"
306
+ end.join
307
+ end
308
+
309
+ failed_info = targeted_report[:failed_element]
310
+ similar_elements = targeted_report[:similar_elements]
311
+
312
+ 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
313
+
314
+ failed_info_content = if failed_info && failed_info[:selector_value]
315
+ "<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>"
316
+ else
317
+ "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído da mensagem de erro.</p>"
318
+ end
319
+
320
+ # Template HTML usando um heredoc
321
+ <<~HTML_REPORT
322
+ <!DOCTYPE html>
323
+ <html lang="pt-BR">
324
+ <head>
325
+ <meta charset="UTF-8">
326
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
327
+ <title>Relatório de Falha Appium - #{timestamp}</title>
328
+ <script src="https://cdn.tailwindcss.com"></script>
329
+ <style>
330
+ body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
331
+ .tab-content { display: none; }
332
+ .tab-content.active { display: block; }
333
+ .tab-button.active { background-color: #4f46e5; color: white; }
334
+ .tab-button:not(.active):hover { background-color: #e0e7ff; }
335
+ </style>
336
+ </head>
337
+ <body class="bg-gray-50 p-8">
338
+ <div class="max-w-7xl mx-auto">
339
+ <header class="mb-8 pb-4 border-b border-gray-300">
340
+ <h1 class="text-3xl font-bold text-gray-800">Diagnóstico de Falha Automatizada</h1>
341
+ <p class="text-sm text-gray-500">Relatório gerado em: #{timestamp} | Plataforma: #{platform.upcase}</p>
342
+ </header>
343
+
344
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
345
+ <!-- Coluna de Screenshots e Falha -->
346
+ <div class="lg:col-span-1">
347
+ <div class="bg-white p-4 rounded-lg shadow-xl mb-6 border border-red-200">
348
+ <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
349
+ #{failed_info_content}
350
+ </div>
351
+
352
+ <div class="bg-white p-4 rounded-lg shadow-xl">
353
+ <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
354
+ <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
355
+ </div>
356
+ </div>
357
+
358
+ <!-- Coluna de Relatórios e Sugestões -->
359
+ <div class="lg:col-span-2">
360
+ <div class="bg-white rounded-lg shadow-xl">
361
+ <!-- Abas de Navegação -->
362
+ <div class="flex border-b border-gray-200">
363
+ <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>
364
+ <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>
365
+ </div>
366
+
367
+ <!-- Conteúdo das Abas -->
368
+ <div class="p-6">
369
+ <!-- Aba Sugestões de Reparo -->
370
+ <div id="similar" class="tab-content active">
371
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Elementos Semelhantes (Alternativas para o Localizador Falho)</h3>
372
+ <div class="space-y-4">
373
+ #{similar_elements_content}
374
+ </div>
375
+ </div>
376
+
377
+ <!-- Aba Dump Completo -->
378
+ <div id="all" class="tab-content">
379
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Dump Completo de Todos os Elementos da Tela</h3>
380
+ <div class="max-h-[600px] overflow-y-auto space-y-2">
381
+ #{all_elements_html.call(all_suggestions)}
382
+ </div>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </div>
389
+
390
+ <script>
391
+ document.addEventListener('DOMContentLoaded', () => {
392
+ const tabs = document.querySelectorAll('.tab-button');
393
+ const contents = document.querySelectorAll('.tab-content');
394
+
395
+ tabs.forEach(tab => {
396
+ tab.addEventListener('click', () => {
397
+ const target = tab.getAttribute('data-tab');
398
+
399
+ tabs.forEach(t => {
400
+ t.classList.remove('active', 'text-white', 'bg-indigo-600');
401
+ t.classList.add('text-gray-600');
402
+ });
403
+ contents.forEach(c => c.classList.remove('active'));
404
+
405
+ tab.classList.add('active', 'text-white', 'bg-indigo-600');
406
+ tab.classList.remove('text-gray-600');
407
+ document.getElementById(target).classList.add('active');
408
+ });
409
+ });
410
+
411
+ // Set initial active state for styling consistency
412
+ const activeTab = document.querySelector('.tab-button[data-tab="similar"]');
413
+ activeTab.classList.add('active', 'text-white', 'bg-indigo-600');
414
+ });
415
+ </script>
416
+ </body>
417
+ </html>
418
+ HTML_REPORT
419
+ end
191
420
  end
192
- end
421
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppiumFailureHelper
4
- VERSION = "0.4.2"
4
+ VERSION = "0.6.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.4.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento