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 +4 -4
- data/README.md +264 -27
- data/lib/appium_failure_helper/capture.rb +258 -29
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb96de86c564a4b997d4d1a4320e831e3d4f28a3f8d7e97623b347d855588303
|
4
|
+
data.tar.gz: 2462b34e6f3c01ea63a8a68d9c1814fd4138dc2c0dbcdbef0aecb8d6ba82fece
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
30
|
+
---
|
17
31
|
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
279
|
+
## Segurança e Privacidade
|
44
280
|
|
45
|
-
|
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
|
-
|
284
|
+
---
|
48
285
|
|
49
|
-
|
286
|
+
## Licença
|
50
287
|
|
51
|
-
|
288
|
+
MIT — veja o arquivo `LICENSE` para os termos.
|
52
289
|
|
53
|
-
|
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 = "
|
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(
|
56
|
+
f.write(Base64.decode64(screenshot_base64))
|
46
57
|
end
|
47
|
-
|
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
|
-
|
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
|
-
|
83
|
+
all_elements_suggestions << { name: name, locators: locators }
|
71
84
|
seen_elements[unique_key] = true
|
72
85
|
end
|
73
86
|
end
|
74
87
|
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
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
|