appium_failure_helper 0.5.0 → 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 +258 -20
- data/lib/appium_failure_helper/capture.rb +138 -129
- 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,32 +1,130 @@
|
|
1
1
|
# Appium Failure Helper
|
2
2
|
|
3
|
-
|
3
|
+
[](https://www.ruby-lang.org/)
|
4
|
+
[](LICENSE)
|
5
|
+
[]()
|
4
6
|
|
5
|
-
|
7
|
+
**Appium Failure Helper** é um módulo Ruby destinado a automatizar diagnóstico de falhas em testes de automação mobile com **Appium**. O objetivo é reduzir tempo de triagem, fornecer localizadores confiáveis e coletar artefatos de depuração sem depender do Appium Inspector.
|
6
8
|
|
7
|
-
|
9
|
+
---
|
8
10
|
|
9
|
-
|
11
|
+
## Sumário
|
12
|
+
- [Visão Geral](#visão-geral)
|
13
|
+
- [Funcionalidades](#funcionalidades)
|
14
|
+
- [Arquitetura e Fluxo](#arquitetura-e-fluxo)
|
15
|
+
- [Instalação](#instalação)
|
16
|
+
- [Configuração (opcional)](#configuração-opcional)
|
17
|
+
- [API Pública / Integração](#api-pública--integração)
|
18
|
+
- [Exemplos de Uso](#exemplos-de-uso)
|
19
|
+
- [Cucumber (hook After)](#cucumber-hook-after)
|
20
|
+
- [RSpec (after :each)](#rspec-after-each)
|
21
|
+
- [Formato dos Artefatos Gerados](#formato-dos-artefatos-gerados)
|
22
|
+
- [Lógica de Geração de XPaths (detalhada)](#lógica-de-geração-de-xpaths-detalhada)
|
23
|
+
- [Tratamento de Dados e Deduplicação](#tratamento-de-dados-e-deduplicação)
|
24
|
+
- [Relatório HTML Interativo](#relatório-html-interativo)
|
25
|
+
- [Logging e Observabilidade](#logging-e-observabilidade)
|
26
|
+
- [Testes e Qualidade](#testes-e-qualidade)
|
27
|
+
- [Roadmap e Contribuição](#roadmap-e-contribuição)
|
28
|
+
- [Licença](#licença)
|
10
29
|
|
11
|
-
|
30
|
+
---
|
12
31
|
|
13
|
-
|
32
|
+
## Visão Geral
|
14
33
|
|
15
|
-
|
34
|
+
No momento em que um teste falha, o módulo realiza, de forma atômica e thread-safe:
|
35
|
+
1. captura de screenshot,
|
36
|
+
2. extração do `page_source` completo (XML),
|
37
|
+
3. varredura da árvore de elementos para gerar localizadores sugeridos,
|
38
|
+
4. escrita de dois YAMLs (focado e completo) e um relatório HTML que agrega tudo.
|
16
39
|
|
17
|
-
|
40
|
+
Todos os artefatos são salvos em uma pasta timestamped (formato `YYYY_MM_DD_HHMMSS`) dentro de `reports_failure/`.
|
18
41
|
|
19
|
-
|
42
|
+
---
|
20
43
|
|
21
|
-
|
44
|
+
## Funcionalidades
|
22
45
|
|
23
|
-
|
46
|
+
- Captura automática de screenshot PNG.
|
47
|
+
- Export completo de `page_source` em XML.
|
48
|
+
- Geração de `failure_analysis_*.yaml` (focado no elemento que falhou).
|
49
|
+
- Geração de `all_elements_dump_*.yaml` (todos os elementos com localizadores sugeridos).
|
50
|
+
- Relatório HTML interativo que combine screenshot, XML formatado e lista de localizadores.
|
51
|
+
- Geração de XPaths otimizados para **Android** e **iOS**.
|
52
|
+
- Truncamento de atributos longos (configurável).
|
53
|
+
- Eliminação de elementos duplicados e normalização de atributos.
|
54
|
+
- Logging via `Logger` do Ruby (Níveis: DEBUG/INFO/WARN/ERROR).
|
55
|
+
- Configuração via bloco `configure` (opcional).
|
24
56
|
|
25
|
-
|
57
|
+
---
|
26
58
|
|
27
|
-
|
59
|
+
## Arquitetura e Fluxo
|
60
|
+
|
61
|
+
1. **Hook de Testes** (Cucumber/RSpec) → invoca `Capture.handler_failure(driver, exception)`
|
62
|
+
2. **Capture.handler_failure**:
|
63
|
+
- estabelece pasta de saída com timestamp;
|
64
|
+
- chama `driver.screenshot` (salva PNG);
|
65
|
+
- chama `driver.page_source` (salva XML);
|
66
|
+
- percorre XML e cria árvore de elementos;
|
67
|
+
- para cada elemento gera candidate XPaths aplicando regras por plataforma;
|
68
|
+
- grava `failure_analysis_*.yaml` (prioriza elemento indicado) e `all_elements_dump_*.yaml`;
|
69
|
+
- monta `report_*.html` agregando tudo.
|
70
|
+
3. Logs detalhados emitidos durante a execução.
|
71
|
+
|
72
|
+
---
|
73
|
+
|
74
|
+
## Instalação
|
75
|
+
|
76
|
+
**Como gem (exemplo):**
|
77
|
+
|
78
|
+
Adicione ao `Gemfile` do projeto:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
gem 'appium_failure_helper', '~> 0.1.0'
|
82
|
+
```
|
83
|
+
|
84
|
+
Depois:
|
85
|
+
|
86
|
+
```bash
|
87
|
+
bundle install
|
88
|
+
```
|
89
|
+
|
90
|
+
**Ou manual (para uso local):**
|
91
|
+
|
92
|
+
Coloque o diretório `appium_failure_helper/` dentro do `lib/` do projeto e faça:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
require_relative 'lib/appium_failure_helper'
|
96
|
+
```
|
97
|
+
|
98
|
+
---
|
99
|
+
|
100
|
+
## API Pública / Integração
|
101
|
+
|
102
|
+
### `AppiumFailureHelper::Capture`
|
28
103
|
|
29
104
|
```ruby
|
105
|
+
# handler_failure(driver, exception, options = {})
|
106
|
+
# - driver: objeto de sessão Appium (Selenium::WebDriver / Appium::Driver)
|
107
|
+
# - exception: exceção capturada no momento da falha
|
108
|
+
# - options: hash com overrides (ex: output_dir:)
|
109
|
+
AppiumFailureHelper::Capture.handler_failure(appium_driver, scenario.exception)
|
110
|
+
```
|
111
|
+
|
112
|
+
### Configuração global
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
AppiumFailureHelper.configure do |c|
|
116
|
+
# ver bloco de configuração acima
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
---
|
121
|
+
|
122
|
+
## Exemplos de Uso
|
123
|
+
|
124
|
+
### Cucumber (hook `After`)
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# features/support/hooks.rb
|
30
128
|
require 'appium_failure_helper'
|
31
129
|
|
32
130
|
After do |scenario|
|
@@ -34,19 +132,159 @@ After do |scenario|
|
|
34
132
|
AppiumFailureHelper::Capture.handler_failure(appium_driver, scenario.exception)
|
35
133
|
end
|
36
134
|
end
|
135
|
+
```
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
## Formato dos Artefatos Gerados
|
140
|
+
|
141
|
+
**Pasta:** `reports_failure/<TIMESTAMP>/`
|
37
142
|
|
143
|
+
Arquivos gerados (ex.: TIMESTAMP = `2025_09_23_173045`):
|
144
|
+
|
145
|
+
```
|
146
|
+
screenshot_2025_09_23_173045.png
|
147
|
+
page_source_2025_09_23_173045.xml
|
148
|
+
failure_analysis_2025_09_23_173045.yaml
|
149
|
+
all_elements_dump_2025_09_23_173045.yaml
|
150
|
+
report_2025_09_23_173045.html
|
151
|
+
```
|
152
|
+
|
153
|
+
### Exemplo (simplificado) de `failure_analysis_*.yaml`
|
154
|
+
|
155
|
+
```yaml
|
156
|
+
failed_element:
|
157
|
+
platform: android
|
158
|
+
summary:
|
159
|
+
class: android.widget.Button
|
160
|
+
resource_id: com.example:id/submit
|
161
|
+
text: "Enviar"
|
162
|
+
suggested_xpaths:
|
163
|
+
- "//android.widget.Button[@resource-id='com.example:id/submit']"
|
164
|
+
- "//android.widget.Button[contains(@text,'Enviar')]"
|
165
|
+
capture_metadata:
|
166
|
+
screenshot: screenshot_2025_09_23_173045.png
|
167
|
+
page_source: page_source_2025_09_23_173045.xml
|
168
|
+
timestamp: "2025-09-23T17:30:45Z"
|
169
|
+
tips: "Priorize resource-id; se ausente, use accessibility id (content-desc) e class+text como fallback."
|
170
|
+
```
|
171
|
+
|
172
|
+
### Exemplo (simplificado) de `all_elements_dump_*.yaml`
|
173
|
+
|
174
|
+
```yaml
|
175
|
+
elements:
|
176
|
+
- id_hash: "a1b2c3..."
|
177
|
+
class: "android.widget.EditText"
|
178
|
+
resource_id: "com.example:id/input_email"
|
179
|
+
text: "example@example.com"
|
180
|
+
truncated_attributes:
|
181
|
+
hint: "Digite seu e-mail..."
|
182
|
+
suggested_xpaths:
|
183
|
+
- "//*[@resource-id='com.example:id/input_email']"
|
184
|
+
- "//android.widget.EditText[contains(@hint,'Digite seu e-mail')]"
|
185
|
+
```
|
186
|
+
|
187
|
+
---
|
188
|
+
|
189
|
+
## Lógica de Geração de XPaths (detalhada)
|
190
|
+
|
191
|
+
**Princípios gerais**
|
192
|
+
1. Priorizar identificadores estáveis (resource-id no Android / accessibility id no iOS).
|
193
|
+
2. Evitar XPaths com `index` como primeira opção (usado apenas como último recurso).
|
194
|
+
3. Combinar atributos quando necessário para aumentar a especificidade e evitar colisões.
|
195
|
+
4. Normalizar espaços e truncar textos longos.
|
196
|
+
|
197
|
+
**Estratégias por plataforma (ordem de preferência)**
|
198
|
+
|
199
|
+
- **Android**
|
200
|
+
1. `resource-id` → `//*[@resource-id='com.pkg:id/id']`
|
201
|
+
2. `content-desc` / `contentDescription` (accessibility) → `//*[@content-desc='x']`
|
202
|
+
3. `class` + `text` → `//android.widget.TextView[@class='...' and contains(normalize-space(@text),'...')]`
|
203
|
+
4. `class` + raça de atributos (combinações: enabled, clickable, package)
|
204
|
+
5. fallback: `//android.widget.Button[position()=n]` (último recurso)
|
205
|
+
|
206
|
+
- **iOS**
|
207
|
+
1. `accessibility id` (nome accessibility) → `//*[@name='Submit']`
|
208
|
+
2. `label` / `value` → `//*[contains(@label,'...')]`
|
209
|
+
3. `type` + `label` → `//XCUIElementTypeButton[@label='OK']`
|
210
|
+
4. fallback: hierarquia / indices
|
211
|
+
|
212
|
+
**Exemplo de XPath combinado (alta especificidade):**
|
213
|
+
|
214
|
+
```xpath
|
215
|
+
//android.widget.Button[@resource-id='com.example:id/submit' and contains(normalize-space(@text),'Enviar') and @clickable='true']
|
38
216
|
```
|
39
217
|
|
40
|
-
|
218
|
+
---
|
219
|
+
|
220
|
+
## Tratamento de Dados e Deduplicação
|
221
|
+
|
222
|
+
- **Truncamento**: atributos com comprimento acima de `attr_truncate_length` são truncados com sufixo `...` para evitar poluição do YAML.
|
223
|
+
- **Hash único por elemento**: é gerado um hash (sha1) baseado em conjunto de atributos relevantes (class+resource-id+content-desc+text) para identificar duplicados.
|
224
|
+
- **Remoção de nulos**: atributos vazios ou nulos são omitidos nos YAMLs.
|
225
|
+
- **Ordenação**: elementos no `all_elements_dump` são ordenados por prioridade de localizador (resource-id primeiro).
|
226
|
+
|
227
|
+
---
|
228
|
+
|
229
|
+
## Relatório HTML Interativo
|
230
|
+
|
231
|
+
O HTML gerado possui:
|
232
|
+
- Visualização inline do `screenshot` (img tag),
|
233
|
+
- Painel colapsável com o `page_source` (XML formatado e collapsible),
|
234
|
+
- Lista navegável de elementos com seus `suggested_xpaths` (botões para copiar),
|
235
|
+
- Ancoragem que permite focalizar: ao clicar em um XPath, realça o fragmento correspondente no XML (se possível),
|
236
|
+
- Metadados e link rápido para os YAMLs.
|
237
|
+
|
238
|
+
**Observação:** o HTML é gerado de forma estática — para realces dinâmicos é usado JavaScript simples embutido (sem dependências externas).
|
239
|
+
|
240
|
+
---
|
241
|
+
|
242
|
+
## Logging e Observabilidade
|
243
|
+
|
244
|
+
- Usa `Logger` padrão do Ruby:
|
245
|
+
- `DEBUG` para detalhamento completo (padrão em modo dev).
|
246
|
+
- `INFO` para resumo das ações realizadas.
|
247
|
+
- `WARN/ERROR` para problemas durante captura/escrita.
|
248
|
+
- Exemplos de mensagens:
|
249
|
+
- `[INFO] Creating failure report folder: reports_failure/2025_09_23_173045`
|
250
|
+
- `[DEBUG] Captured 4123 elements from page_source`
|
251
|
+
- `[ERROR] Failed to write screenshot: Permission denied`
|
252
|
+
|
253
|
+
---
|
254
|
+
|
255
|
+
## Testes e Qualidade
|
256
|
+
|
257
|
+
- Estrutura de testes sugerida: RSpec + fixtures com dumps de `page_source` para validar a geração de XPaths.
|
258
|
+
- Testes unitários para: truncamento, hash de deduplicação, geração de strategies, output YAML válido.
|
259
|
+
- CI: incluir step que valide YAML/HTML gerados (lint) e execute testes RSpec.
|
260
|
+
|
261
|
+
---
|
262
|
+
|
263
|
+
## Roadmap e Contribuição
|
264
|
+
|
265
|
+
**Funcionalidades previstas**
|
266
|
+
- Suporte a mapeamento visual (overlay) para apontar elemento sobre screenshot.
|
267
|
+
- Export para outros formatos (JSON/CSV).
|
268
|
+
- Integração com ferramentas de observabilidade (Sentry, Datadog).
|
269
|
+
- Modo headless para gerar relatórios offline em pipelines.
|
270
|
+
|
271
|
+
**Como contribuir**
|
272
|
+
1. Fork no repositório.
|
273
|
+
2. Crie branch com feature/bugfix.
|
274
|
+
3. Abra PR com descrição técnica das mudanças e testes.
|
275
|
+
4. Mantenha o estilo Ruby (RuboCop) e documentação atualizada.
|
276
|
+
|
277
|
+
---
|
41
278
|
|
42
|
-
##
|
279
|
+
## Segurança e Privacidade
|
43
280
|
|
44
|
-
|
281
|
+
- Evite capturar dados sensíveis em ambientes com PII. Implementar filtro por regex para mascarar dados (ex.: emails/telefones) antes de salvar YAMLs.
|
282
|
+
- Recomendado: executar limpeza em ambientes de produção.
|
45
283
|
|
46
|
-
|
284
|
+
---
|
47
285
|
|
48
|
-
|
286
|
+
## Licença
|
49
287
|
|
50
|
-
|
288
|
+
MIT — veja o arquivo `LICENSE` para os termos.
|
51
289
|
|
52
|
-
|
290
|
+
---
|
@@ -114,7 +114,6 @@ module AppiumFailureHelper
|
|
114
114
|
File.write(html_report_path, html_content)
|
115
115
|
@@logger.info("Relatório HTML completo salvo em #{html_report_path}")
|
116
116
|
|
117
|
-
|
118
117
|
rescue => e
|
119
118
|
@@logger.error("Erro ao capturar detalhes da falha: #{e.message}\n#{e.backtrace.join("\n")}")
|
120
119
|
end
|
@@ -129,134 +128,24 @@ module AppiumFailureHelper
|
|
129
128
|
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
130
129
|
end
|
131
130
|
end
|
132
|
-
|
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>
|
181
|
-
|
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>
|
187
|
-
|
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>
|
194
|
-
|
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>
|
203
|
-
|
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>
|
215
|
-
|
216
|
-
<script>
|
217
|
-
document.addEventListener('DOMContentLoaded', () => {
|
218
|
-
const tabs = document.querySelectorAll('.tab-button');
|
219
|
-
const contents = document.querySelectorAll('.tab-content');
|
220
|
-
|
221
|
-
tabs.forEach(tab => {
|
222
|
-
tab.addEventListener('click', () => {
|
223
|
-
const target = tab.getAttribute('data-tab');
|
224
|
-
|
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'));
|
227
|
-
|
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
|
240
|
-
end
|
241
|
-
|
242
|
-
# --- Métodos de Suporte Existentes ---
|
243
131
|
|
244
|
-
# ... (métodos setup_logger, extract_info_from_exception, find_similar_elements, etc.)
|
245
|
-
|
246
132
|
def self.extract_info_from_exception(exception)
|
247
133
|
message = exception.message
|
248
134
|
info = {}
|
249
135
|
|
136
|
+
# Corrigido: Usando múltiplos padrões para extração robusta (resolvendo o problema do YAML vazio)
|
250
137
|
patterns = [
|
251
|
-
/(?:could not be found|cannot find element) using (.+)=['"](.+)['"]
|
252
|
-
/no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i
|
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
|
253
142
|
]
|
254
143
|
|
255
144
|
patterns.each do |pattern|
|
256
145
|
match = message.match(pattern)
|
257
146
|
if match
|
258
|
-
|
259
|
-
|
147
|
+
selector_value = match.captures.last.strip
|
148
|
+
selector_type = match[1]&.strip || 'Unknown'
|
260
149
|
|
261
150
|
info[:selector_type] = selector_type
|
262
151
|
info[:selector_value] = selector_value.gsub(/['"]/, '')
|
@@ -272,15 +161,17 @@ module AppiumFailureHelper
|
|
272
161
|
next if node.name == 'hierarchy'
|
273
162
|
attrs = node.attributes.transform_values(&:value)
|
274
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
|
275
166
|
is_similar = case platform
|
276
167
|
when 'android'
|
277
|
-
(attrs['resource-id']&.include?(
|
278
|
-
attrs['text']&.include?(
|
279
|
-
attrs['content-desc']&.include?(
|
168
|
+
(attrs['resource-id']&.downcase&.include?(selector_value) ||
|
169
|
+
attrs['text']&.downcase&.include?(selector_value) ||
|
170
|
+
attrs['content-desc']&.downcase&.include?(selector_value))
|
280
171
|
when 'ios'
|
281
|
-
(attrs['accessibility-id']&.include?(
|
282
|
-
attrs['label']&.include?(
|
283
|
-
attrs['name']&.include?(
|
172
|
+
(attrs['accessibility-id']&.downcase&.include?(selector_value) ||
|
173
|
+
attrs['label']&.downcase&.include?(selector_value) ||
|
174
|
+
attrs['name']&.downcase&.include?(selector_value))
|
284
175
|
else
|
285
176
|
false
|
286
177
|
end
|
@@ -400,13 +291,131 @@ module AppiumFailureHelper
|
|
400
291
|
|
401
292
|
locators
|
402
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
|
403
302
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
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
|
409
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
|
410
419
|
end
|
411
420
|
end
|
412
421
|
end
|