siesa-agents 2.1.72-qa.2 → 2.1.72-qa.21
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.
- package/bmad/bmm/config.yaml +33 -0
- package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
- package/claude/skills/sa-aplicar/SKILL.md +268 -0
- package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
- package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
- package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
- package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
- package/claude/skills/sa-onboard-db/SKILL.md +122 -0
- package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
- package/claude/skills/sa-quality-process/SKILL.md +6 -0
- package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
- package/package.json +1 -1
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
- package/siesa-agents/resources/playwright-kit/.env.example +42 -0
- package/siesa-agents/resources/playwright-kit/README.md +228 -0
- package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
- package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
- package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
- package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
- package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
- package/siesa-agents/resources/playwright-kit/package.json +27 -0
- package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
- package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
- package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
- package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
- package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
- package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
- package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
- package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
- package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
- package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
- package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
- package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
- package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
- package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
- package/siesa-agents/scripts/merge_test_design.py +106 -106
- package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
- package/siesa-agents/scripts/requirements.txt +7 -0
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay del Grabador de Localizadores v4.0
|
|
3
|
+
* Dos modos: Inspect (Ctrl+Click) y Record (automático)
|
|
4
|
+
*
|
|
5
|
+
* Funciones expuestas esperadas (via page.exposeFunction):
|
|
6
|
+
* window.__lrSaveJSON(json) → { filename, filePath }
|
|
7
|
+
* window.__lrGenerateTS(json) → { filename, filePath, content }
|
|
8
|
+
* window.__lrCaptureScreenshot(stepNum) → { filename, filePath } (v4)
|
|
9
|
+
* window.__lrSaveModule(json) → { filename, filePath } (v4)
|
|
10
|
+
* window.__lrListModules() → JSON string[] (v4)
|
|
11
|
+
* window.__lrLoadModule(name) → JSON string (v4)
|
|
12
|
+
*
|
|
13
|
+
* Mejoras v4.0:
|
|
14
|
+
* - Assertions capturadas: clic derecho → menu contextual (texto/existe/valor)
|
|
15
|
+
* - Screenshots / visual baseline: boton Screenshot → toHaveScreenshot()
|
|
16
|
+
* - Modulos reutilizables: guardar/insertar secuencias JSON entre grabaciones
|
|
17
|
+
*/
|
|
18
|
+
(() => {
|
|
19
|
+
if (window.__locatorRecorder) return;
|
|
20
|
+
window.__locatorRecorder = true;
|
|
21
|
+
|
|
22
|
+
// Restaurar capturas de sessionStorage
|
|
23
|
+
let stored = null;
|
|
24
|
+
try { stored = sessionStorage.getItem('__lr_captures'); } catch {}
|
|
25
|
+
window.__capturedElements = stored ? JSON.parse(stored) : [];
|
|
26
|
+
|
|
27
|
+
function persistCaptures() {
|
|
28
|
+
try { sessionStorage.setItem('__lr_captures', JSON.stringify(window.__capturedElements)); } catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Estado de grabación (persistido en sessionStorage) ──
|
|
32
|
+
let isRecording = false;
|
|
33
|
+
let _lastRecSelector = null;
|
|
34
|
+
let _lastRecTime = 0;
|
|
35
|
+
let _lastRecElement = null;
|
|
36
|
+
|
|
37
|
+
function persistRecState() {
|
|
38
|
+
try { sessionStorage.setItem('__lr_recording', isRecording ? '1' : '0'); } catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════
|
|
42
|
+
// ESTILOS
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════
|
|
44
|
+
const style = document.createElement('style');
|
|
45
|
+
style.textContent = `
|
|
46
|
+
#lr-tooltip {
|
|
47
|
+
position: fixed; z-index: 999999; pointer-events: none;
|
|
48
|
+
background: #1a1a2e; color: #eee; border: 2px solid #00ff88;
|
|
49
|
+
border-radius: 8px; padding: 10px 14px; font-family: 'Consolas', monospace;
|
|
50
|
+
font-size: 12px; max-width: 520px; box-shadow: 0 4px 20px rgba(0,255,136,0.3);
|
|
51
|
+
display: none; line-height: 1.5;
|
|
52
|
+
}
|
|
53
|
+
#lr-tooltip .lr-label { color: #888; font-size: 10px; text-transform: uppercase; }
|
|
54
|
+
#lr-tooltip .lr-value { color: #00ff88; font-weight: bold; }
|
|
55
|
+
#lr-tooltip .lr-value.lr-best { color: #ffd700; }
|
|
56
|
+
#lr-tooltip .lr-disabled { color: #ff4757; }
|
|
57
|
+
|
|
58
|
+
#lr-highlight {
|
|
59
|
+
position: fixed; z-index: 999998; pointer-events: none;
|
|
60
|
+
border: 2px dashed #00ff88; background: rgba(0,255,136,0.08);
|
|
61
|
+
border-radius: 3px; transition: all 0.1s ease; display: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#lr-panel {
|
|
65
|
+
position: fixed; top: 10px; right: 10px; z-index: 999997;
|
|
66
|
+
background: #1a1a2e; color: #eee; border: 2px solid #00b894;
|
|
67
|
+
border-radius: 10px; width: 440px; max-height: 90vh;
|
|
68
|
+
font-family: 'Segoe UI', sans-serif; font-size: 13px;
|
|
69
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5); overflow: hidden;
|
|
70
|
+
display: flex; flex-direction: column;
|
|
71
|
+
transition: border-color 0.3s, opacity 0.3s;
|
|
72
|
+
opacity: 0.25;
|
|
73
|
+
}
|
|
74
|
+
#lr-panel:hover {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
#lr-panel-header {
|
|
78
|
+
background: #16213e; padding: 8px 12px; cursor: move;
|
|
79
|
+
border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center;
|
|
80
|
+
}
|
|
81
|
+
#lr-panel-header h3 { margin: 0; font-size: 13px; color: #00ff88; }
|
|
82
|
+
#lr-panel-header .lr-badge {
|
|
83
|
+
background: #00b894; color: #fff; border-radius: 12px; padding: 2px 8px; font-size: 10px;
|
|
84
|
+
}
|
|
85
|
+
.lr-window-btn {
|
|
86
|
+
background: none; border: 1px solid #555; color: #aaa; width: 22px; height: 22px;
|
|
87
|
+
border-radius: 4px; cursor: pointer; font-size: 13px; line-height: 1;
|
|
88
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
89
|
+
transition: all 0.15s; margin-left: 4px;
|
|
90
|
+
}
|
|
91
|
+
.lr-window-btn:hover { background: #333; color: #fff; border-color: #888; }
|
|
92
|
+
|
|
93
|
+
#lr-panel-recording {
|
|
94
|
+
padding: 8px 16px; background: #16213e; border-bottom: 1px solid #333;
|
|
95
|
+
display: flex; gap: 8px; align-items: center;
|
|
96
|
+
}
|
|
97
|
+
.lr-btn-rec {
|
|
98
|
+
padding: 5px 14px; border-radius: 5px; cursor: pointer;
|
|
99
|
+
font-size: 11px; font-weight: 700; transition: all 0.2s; border: 1px solid;
|
|
100
|
+
}
|
|
101
|
+
.lr-btn-rec:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
102
|
+
.lr-btn-rec-start {
|
|
103
|
+
background: rgba(249,115,22,0.15); color: #f97316; border-color: #f97316;
|
|
104
|
+
}
|
|
105
|
+
.lr-btn-rec-start:hover:not(:disabled) { background: rgba(249,115,22,0.3); }
|
|
106
|
+
.lr-btn-rec-stop {
|
|
107
|
+
background: rgba(239,68,68,0.15); color: #ef4444; border-color: #ef4444;
|
|
108
|
+
}
|
|
109
|
+
.lr-btn-rec-stop:hover:not(:disabled) { background: rgba(239,68,68,0.3); }
|
|
110
|
+
#lr-rec-status {
|
|
111
|
+
font-family: Consolas, monospace; font-size: 10px; font-weight: 700;
|
|
112
|
+
padding: 2px 8px; border-radius: 4px; letter-spacing: 1px;
|
|
113
|
+
}
|
|
114
|
+
.lr-rec-active {
|
|
115
|
+
background: rgba(239,68,68,0.15); color: #ef4444; border: 1px solid #ef4444;
|
|
116
|
+
animation: lr-status-pulse 1.5s ease-in-out infinite;
|
|
117
|
+
}
|
|
118
|
+
.lr-rec-idle { color: #888; }
|
|
119
|
+
@keyframes lr-status-pulse {
|
|
120
|
+
0%,100% { opacity: 1; } 50% { opacity: 0.5; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#lr-panel-group {
|
|
124
|
+
padding: 6px 16px; background: #16213e; border-bottom: 1px solid #333;
|
|
125
|
+
display: flex; gap: 8px; align-items: center;
|
|
126
|
+
}
|
|
127
|
+
#lr-panel-group .lr-group-label { color: #888; font-size: 11px; white-space: nowrap; }
|
|
128
|
+
#lr-panel-group input {
|
|
129
|
+
flex: 1; background: #0a0a1a; color: #ffd700; border: 1px solid #444;
|
|
130
|
+
border-radius: 4px; padding: 4px 8px; font-size: 12px;
|
|
131
|
+
font-family: Consolas, monospace; outline: none;
|
|
132
|
+
}
|
|
133
|
+
#lr-panel-group input:focus { border-color: #ffd700; }
|
|
134
|
+
#lr-panel-group .lr-group-info { color: #888; font-size: 10px; white-space: nowrap; }
|
|
135
|
+
|
|
136
|
+
#lr-panel-actions {
|
|
137
|
+
padding: 8px 16px; background: #16213e; border-bottom: 1px solid #333;
|
|
138
|
+
display: flex; gap: 6px; flex-wrap: wrap;
|
|
139
|
+
}
|
|
140
|
+
#lr-panel-actions button {
|
|
141
|
+
padding: 5px 12px; border: none; border-radius: 5px; cursor: pointer;
|
|
142
|
+
font-size: 11px; font-weight: 600; transition: opacity 0.2s;
|
|
143
|
+
}
|
|
144
|
+
#lr-panel-actions button:hover { opacity: 0.85; }
|
|
145
|
+
.lr-btn-detect { background: #6c5ce7; color: #fff; }
|
|
146
|
+
.lr-btn-clear { background: #e74c3c; color: #fff; }
|
|
147
|
+
.lr-btn-export { background: #0984e3; color: #fff; }
|
|
148
|
+
.lr-btn-copy { background: #00b894; color: #fff; }
|
|
149
|
+
.lr-btn-toggle { background: #fdcb6e; color: #333; }
|
|
150
|
+
|
|
151
|
+
#lr-panel-list { overflow-y: auto; flex: 1; padding: 8px 12px; max-height: 50vh; }
|
|
152
|
+
|
|
153
|
+
.lr-group-header {
|
|
154
|
+
color: #ffd700; font-weight: bold; font-size: 12px;
|
|
155
|
+
padding: 8px 4px 4px; border-bottom: 1px solid #333;
|
|
156
|
+
margin-top: 8px; display: flex; justify-content: space-between;
|
|
157
|
+
}
|
|
158
|
+
.lr-group-header:first-child { margin-top: 0; }
|
|
159
|
+
.lr-group-header .lr-group-count { color: #888; font-weight: normal; font-size: 10px; }
|
|
160
|
+
|
|
161
|
+
.lr-item {
|
|
162
|
+
background: #0f3460; border-radius: 6px; padding: 10px 12px; margin-bottom: 8px;
|
|
163
|
+
border-left: 4px solid #00b894; position: relative;
|
|
164
|
+
}
|
|
165
|
+
.lr-item:hover { background: #1a4a7a; }
|
|
166
|
+
.lr-item-click { border-left-color: #f97316; }
|
|
167
|
+
.lr-item-fill { border-left-color: #3b82f6; }
|
|
168
|
+
.lr-item-select { border-left-color: #a78bfa; }
|
|
169
|
+
.lr-item-disabled { border-left-color: #ff4757; }
|
|
170
|
+
.lr-item .lr-item-num { color: #00ff88; font-weight: bold; font-size: 11px; }
|
|
171
|
+
.lr-item .lr-item-tag { color: #ddd; font-size: 12px; }
|
|
172
|
+
.lr-item .lr-item-loc {
|
|
173
|
+
color: #ffd700; font-family: Consolas, monospace; font-size: 11px;
|
|
174
|
+
word-break: break-all; margin-top: 4px;
|
|
175
|
+
}
|
|
176
|
+
.lr-item .lr-item-extra { color: #aaa; font-size: 10px; margin-top: 2px; }
|
|
177
|
+
.lr-item .lr-item-value { color: #22c55e; font-family: Consolas, monospace; font-size: 11px; margin-top: 3px; }
|
|
178
|
+
.lr-item .lr-item-copy {
|
|
179
|
+
position: absolute; top: 8px; right: 8px; background: #00b894; color: #fff;
|
|
180
|
+
border: none; border-radius: 4px; padding: 2px 8px; cursor: pointer; font-size: 10px;
|
|
181
|
+
}
|
|
182
|
+
.lr-item .lr-item-delete {
|
|
183
|
+
position: absolute; top: 8px; right: 56px; background: #e74c3c; color: #fff;
|
|
184
|
+
border: none; border-radius: 4px; padding: 2px 6px; cursor: pointer; font-size: 10px; font-weight: bold;
|
|
185
|
+
}
|
|
186
|
+
.lr-action-badge {
|
|
187
|
+
display: inline-block; font-size: 9px; font-weight: 700; padding: 1px 6px;
|
|
188
|
+
border-radius: 3px; letter-spacing: 0.5px; margin-left: 6px;
|
|
189
|
+
}
|
|
190
|
+
.lr-action-click { background: rgba(249,115,22,0.2); color: #f97316; }
|
|
191
|
+
.lr-action-fill { background: rgba(59,130,246,0.2); color: #3b82f6; }
|
|
192
|
+
.lr-action-select { background: rgba(167,139,250,0.2); color: #a78bfa; }
|
|
193
|
+
|
|
194
|
+
#lr-panel-footer {
|
|
195
|
+
padding: 8px 16px; background: #16213e; border-top: 1px solid #333;
|
|
196
|
+
font-size: 10px; color: #888; text-align: center;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#lr-status {
|
|
200
|
+
position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
|
|
201
|
+
z-index: 999996; background: #1a1a2e; color: #00ff88; border: 1px solid #00b894;
|
|
202
|
+
border-radius: 20px; padding: 6px 18px; font-family: 'Segoe UI', sans-serif;
|
|
203
|
+
font-size: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
204
|
+
transition: opacity 0.3s; opacity: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#lr-stop-btn {
|
|
208
|
+
position: fixed; bottom: 20px; right: 20px;
|
|
209
|
+
background: #ef4444; color: #fff;
|
|
210
|
+
padding: 11px 20px; border-radius: 8px;
|
|
211
|
+
cursor: pointer; z-index: 2147483647;
|
|
212
|
+
border: 2px solid #fca5a5;
|
|
213
|
+
display: flex; align-items: center; gap: 8px;
|
|
214
|
+
font-size: 14px; user-select: none;
|
|
215
|
+
transition: all 0.2s;
|
|
216
|
+
animation: lr-rec-pulse 1.5s ease-in-out infinite;
|
|
217
|
+
}
|
|
218
|
+
#lr-stop-btn:hover { background: #b91c1c; transform: scale(1.06); }
|
|
219
|
+
@keyframes lr-rec-pulse {
|
|
220
|
+
0%,100% { box-shadow: 0 4px 20px rgba(239,68,68,0.5); }
|
|
221
|
+
50% { box-shadow: 0 4px 32px rgba(239,68,68,0.95); transform: scale(1.04); }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ── v4: Mejora 1 — Menu contextual de assertions (clic derecho) ── */
|
|
225
|
+
#lr-context-menu {
|
|
226
|
+
position: fixed; z-index: 2147483647; display: none;
|
|
227
|
+
background: #0a2a1e; color: #eee; border: 2px solid #10b981;
|
|
228
|
+
border-radius: 8px; padding: 6px 0; min-width: 230px;
|
|
229
|
+
font-family: 'Segoe UI', sans-serif; font-size: 12px;
|
|
230
|
+
box-shadow: 0 8px 28px rgba(16,185,129,0.35);
|
|
231
|
+
}
|
|
232
|
+
#lr-context-menu .lr-ctx-title {
|
|
233
|
+
color: #10b981; font-weight: 700; font-size: 10px; letter-spacing: 1px;
|
|
234
|
+
padding: 4px 14px 6px; border-bottom: 1px solid #134e3a; margin-bottom: 4px;
|
|
235
|
+
text-transform: uppercase;
|
|
236
|
+
}
|
|
237
|
+
#lr-context-menu .lr-ctx-item {
|
|
238
|
+
padding: 7px 14px; cursor: pointer; white-space: nowrap;
|
|
239
|
+
overflow: hidden; text-overflow: ellipsis; max-width: 340px;
|
|
240
|
+
}
|
|
241
|
+
#lr-context-menu .lr-ctx-item:hover { background: rgba(16,185,129,0.18); }
|
|
242
|
+
#lr-context-menu .lr-ctx-item .lr-ctx-val { color: #6ee7b7; font-family: Consolas, monospace; }
|
|
243
|
+
#lr-context-menu .lr-ctx-cancel { color: #888; border-top: 1px solid #134e3a; margin-top: 4px; }
|
|
244
|
+
|
|
245
|
+
/* badge + item assert en el panel */
|
|
246
|
+
.lr-action-assert { background: rgba(16,185,129,0.2); color: #10b981; }
|
|
247
|
+
.lr-action-screenshot { background: rgba(245,158,11,0.2); color: #f59e0b; }
|
|
248
|
+
.lr-item-assert { border-left-color: #10b981; }
|
|
249
|
+
.lr-item-screenshot { border-left-color: #f59e0b; }
|
|
250
|
+
.lr-item .lr-item-assert-detail { color: #10b981; font-family: Consolas, monospace; font-size: 11px; margin-top: 3px; }
|
|
251
|
+
.lr-item .lr-item-screenshot-detail { color: #f59e0b; font-family: Consolas, monospace; font-size: 11px; margin-top: 3px; }
|
|
252
|
+
|
|
253
|
+
/* ── v4: Mejora 2 — Boton Screenshot (amber) ── */
|
|
254
|
+
.lr-btn-screenshot { background: #f59e0b; color: #1a1a2e; }
|
|
255
|
+
|
|
256
|
+
/* ── v4: Mejora 3 — Modulos reutilizables (violeta) ── */
|
|
257
|
+
.lr-btn-module { background: #8b5cf6; color: #fff; }
|
|
258
|
+
#lr-module-dialog {
|
|
259
|
+
position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
260
|
+
z-index: 2147483647; display: none;
|
|
261
|
+
background: #1a1530; color: #eee; border: 2px solid #8b5cf6;
|
|
262
|
+
border-radius: 10px; padding: 18px 20px; width: 380px;
|
|
263
|
+
font-family: 'Segoe UI', sans-serif; font-size: 13px;
|
|
264
|
+
box-shadow: 0 12px 40px rgba(139,92,246,0.4);
|
|
265
|
+
}
|
|
266
|
+
#lr-module-dialog h4 { margin: 0 0 12px; color: #c4b5fd; font-size: 14px; }
|
|
267
|
+
#lr-module-dialog label { display: block; color: #aaa; font-size: 11px; margin: 8px 0 3px; }
|
|
268
|
+
#lr-module-dialog input, #lr-module-dialog textarea {
|
|
269
|
+
width: 100%; box-sizing: border-box; background: #0a0a1a; color: #c4b5fd;
|
|
270
|
+
border: 1px solid #4c3a7a; border-radius: 5px; padding: 6px 9px;
|
|
271
|
+
font-family: Consolas, monospace; font-size: 12px; outline: none;
|
|
272
|
+
}
|
|
273
|
+
#lr-module-dialog input:focus, #lr-module-dialog textarea:focus { border-color: #8b5cf6; }
|
|
274
|
+
#lr-module-dialog textarea { resize: vertical; min-height: 50px; }
|
|
275
|
+
#lr-module-dialog .lr-module-buttons {
|
|
276
|
+
display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px;
|
|
277
|
+
}
|
|
278
|
+
#lr-module-dialog .lr-module-buttons button {
|
|
279
|
+
padding: 6px 16px; border: none; border-radius: 5px; cursor: pointer;
|
|
280
|
+
font-size: 12px; font-weight: 600;
|
|
281
|
+
}
|
|
282
|
+
#lr-module-cancel { background: #e74c3c; color: #fff; }
|
|
283
|
+
#lr-module-confirm { background: #8b5cf6; color: #fff; }
|
|
284
|
+
#lr-module-list {
|
|
285
|
+
position: fixed; z-index: 2147483647; display: none;
|
|
286
|
+
background: #1a1530; color: #eee; border: 2px solid #8b5cf6;
|
|
287
|
+
border-radius: 8px; padding: 6px 0; min-width: 260px; max-height: 50vh;
|
|
288
|
+
overflow-y: auto; font-family: 'Segoe UI', sans-serif; font-size: 12px;
|
|
289
|
+
box-shadow: 0 8px 28px rgba(139,92,246,0.35);
|
|
290
|
+
}
|
|
291
|
+
#lr-module-list .lr-modlist-title {
|
|
292
|
+
color: #c4b5fd; font-weight: 700; font-size: 10px; letter-spacing: 1px;
|
|
293
|
+
padding: 4px 14px 6px; border-bottom: 1px solid #4c3a7a; margin-bottom: 4px;
|
|
294
|
+
text-transform: uppercase;
|
|
295
|
+
}
|
|
296
|
+
#lr-module-list .lr-modlist-item { padding: 7px 14px; cursor: pointer; }
|
|
297
|
+
#lr-module-list .lr-modlist-item:hover { background: rgba(139,92,246,0.18); }
|
|
298
|
+
#lr-module-list .lr-modlist-item .lr-modlist-meta { color: #888; font-size: 10px; }
|
|
299
|
+
#lr-module-list .lr-modlist-empty { padding: 10px 14px; color: #888; }
|
|
300
|
+
`;
|
|
301
|
+
document.head.appendChild(style);
|
|
302
|
+
|
|
303
|
+
// ═══════════════════════════════════════════════════════════════
|
|
304
|
+
// ELEMENTOS DOM
|
|
305
|
+
// ═══════════════════════════════════════════════════════════════
|
|
306
|
+
const tooltip = document.createElement('div');
|
|
307
|
+
tooltip.id = 'lr-tooltip';
|
|
308
|
+
document.body.appendChild(tooltip);
|
|
309
|
+
|
|
310
|
+
const highlight = document.createElement('div');
|
|
311
|
+
highlight.id = 'lr-highlight';
|
|
312
|
+
document.body.appendChild(highlight);
|
|
313
|
+
|
|
314
|
+
const statusBar = document.createElement('div');
|
|
315
|
+
statusBar.id = 'lr-status';
|
|
316
|
+
document.body.appendChild(statusBar);
|
|
317
|
+
|
|
318
|
+
const panel = document.createElement('div');
|
|
319
|
+
panel.id = 'lr-panel';
|
|
320
|
+
panel.innerHTML = `
|
|
321
|
+
<div id="lr-panel-header">
|
|
322
|
+
<h3>Grabador v4</h3>
|
|
323
|
+
<div style="display:flex;align-items:center;gap:4px;">
|
|
324
|
+
<span class="lr-badge" id="lr-count">0</span>
|
|
325
|
+
<button class="lr-window-btn" id="lr-btn-minimize" title="Minimizar (Esc)">−</button>
|
|
326
|
+
<button class="lr-window-btn" id="lr-btn-maximize" title="Maximizar / Restaurar">□</button>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div id="lr-panel-recording">
|
|
330
|
+
<button class="lr-btn-rec lr-btn-rec-start" id="lr-btn-record">⏺ Grabar</button>
|
|
331
|
+
<button class="lr-btn-rec lr-btn-rec-stop" id="lr-btn-stop" disabled>⏹ Detener</button>
|
|
332
|
+
<span id="lr-rec-status" class="lr-rec-idle">LISTO</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div id="lr-panel-group">
|
|
335
|
+
<span class="lr-group-label">Grupo:</span>
|
|
336
|
+
<input id="lr-group-input" type="text" value="General" placeholder="Nombre del grupo..." />
|
|
337
|
+
<span class="lr-group-info" id="lr-group-info"></span>
|
|
338
|
+
</div>
|
|
339
|
+
<div id="lr-panel-actions">
|
|
340
|
+
<button class="lr-btn-detect" id="lr-act-detect">Detectar</button>
|
|
341
|
+
<button class="lr-btn-copy" id="lr-act-copy">Copiar</button>
|
|
342
|
+
<button class="lr-btn-export" id="lr-act-json">JSON</button>
|
|
343
|
+
<button class="lr-btn-export" id="lr-act-ts">PW TS</button>
|
|
344
|
+
<button class="lr-btn-export" id="lr-act-robot">.robot</button>
|
|
345
|
+
<button class="lr-btn-export" id="lr-act-pwpy">PW PY</button>
|
|
346
|
+
<button class="lr-btn-export" id="lr-act-cypress">Cypress</button>
|
|
347
|
+
<button class="lr-btn-export" id="lr-act-gherkin">Gherkin</button>
|
|
348
|
+
<button class="lr-btn-export" id="lr-act-csv">CSV</button>
|
|
349
|
+
<button class="lr-btn-screenshot" id="lr-act-screenshot">Screenshot</button>
|
|
350
|
+
<button class="lr-btn-module" id="lr-act-save-module">Guardar modulo</button>
|
|
351
|
+
<button class="lr-btn-module" id="lr-act-insert-module">Insertar modulo</button>
|
|
352
|
+
<button class="lr-btn-clear" id="lr-act-clear">Limpiar</button>
|
|
353
|
+
</div>
|
|
354
|
+
<div id="lr-panel-list"></div>
|
|
355
|
+
<div id="lr-preview-container" style="display:none; padding:8px 12px;">
|
|
356
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
|
357
|
+
<span style="color:#ffd700;font-size:12px;font-weight:bold;" id="lr-preview-title">Preview</span>
|
|
358
|
+
<div>
|
|
359
|
+
<button id="lr-preview-copy" style="background:#00b894;color:#fff;border:none;border-radius:4px;padding:3px 10px;cursor:pointer;font-size:11px;margin-right:4px;">Copiar</button>
|
|
360
|
+
<button id="lr-preview-close" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:3px 10px;cursor:pointer;font-size:11px;">Cerrar</button>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<textarea id="lr-preview-text" style="width:100%;height:250px;background:#0a0a1a;color:#00ff88;border:1px solid #333;border-radius:4px;font-family:Consolas,monospace;font-size:11px;padding:8px;resize:vertical;" readonly></textarea>
|
|
364
|
+
</div>
|
|
365
|
+
<div id="lr-panel-footer">Grabar = captura auto | Ctrl+Click = inspeccionar | Clic derecho = assert | Esc = minimizar</div>
|
|
366
|
+
`;
|
|
367
|
+
document.body.appendChild(panel);
|
|
368
|
+
|
|
369
|
+
// ── v4: Menu contextual de assertions (clic derecho) ──
|
|
370
|
+
const contextMenu = document.createElement('div');
|
|
371
|
+
contextMenu.id = 'lr-context-menu';
|
|
372
|
+
document.body.appendChild(contextMenu);
|
|
373
|
+
|
|
374
|
+
// ── v4: Dialog para guardar modulo ──
|
|
375
|
+
const moduleDialog = document.createElement('div');
|
|
376
|
+
moduleDialog.id = 'lr-module-dialog';
|
|
377
|
+
moduleDialog.innerHTML = `
|
|
378
|
+
<h4>Guardar modulo reutilizable</h4>
|
|
379
|
+
<label for="lr-module-name">Nombre (sin espacios, ej: login-erp)</label>
|
|
380
|
+
<input id="lr-module-name" type="text" placeholder="login-erp" />
|
|
381
|
+
<label for="lr-module-desc">Descripcion (opcional)</label>
|
|
382
|
+
<textarea id="lr-module-desc" placeholder="Login al ERP con usuario de pruebas"></textarea>
|
|
383
|
+
<div class="lr-module-buttons">
|
|
384
|
+
<button id="lr-module-cancel">Cancelar</button>
|
|
385
|
+
<button id="lr-module-confirm">Guardar</button>
|
|
386
|
+
</div>
|
|
387
|
+
`;
|
|
388
|
+
document.body.appendChild(moduleDialog);
|
|
389
|
+
|
|
390
|
+
// ── v4: Dropdown para insertar modulo ──
|
|
391
|
+
const moduleList = document.createElement('div');
|
|
392
|
+
moduleList.id = 'lr-module-list';
|
|
393
|
+
document.body.appendChild(moduleList);
|
|
394
|
+
|
|
395
|
+
// Vincular botones con addEventListener (más robusto que onclick inline)
|
|
396
|
+
document.getElementById('lr-btn-record').addEventListener('click', (e) => { e.stopPropagation(); window.__lrStartRec(); });
|
|
397
|
+
document.getElementById('lr-btn-stop').addEventListener('click', (e) => { e.stopPropagation(); window.__lrStopRec(); });
|
|
398
|
+
document.getElementById('lr-act-detect').addEventListener('click', (e) => { e.stopPropagation(); window.__lrDetectVisible(); });
|
|
399
|
+
document.getElementById('lr-act-copy').addEventListener('click', (e) => { e.stopPropagation(); window.__lrCopyAll(); });
|
|
400
|
+
document.getElementById('lr-act-json').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportJSON(); });
|
|
401
|
+
document.getElementById('lr-act-ts').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportTS(); });
|
|
402
|
+
document.getElementById('lr-act-robot').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportRobot(); });
|
|
403
|
+
document.getElementById('lr-act-pwpy').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportPwPy(); });
|
|
404
|
+
document.getElementById('lr-act-cypress').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportCypress(); });
|
|
405
|
+
document.getElementById('lr-act-gherkin').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportGherkin(); });
|
|
406
|
+
document.getElementById('lr-act-csv').addEventListener('click', (e) => { e.stopPropagation(); window.__lrExportCSV(); });
|
|
407
|
+
document.getElementById('lr-act-screenshot').addEventListener('click', (e) => { e.stopPropagation(); window.__lrTakeScreenshot(); });
|
|
408
|
+
document.getElementById('lr-act-save-module').addEventListener('click', (e) => { e.stopPropagation(); window.__lrShowSaveModule(); });
|
|
409
|
+
document.getElementById('lr-act-insert-module').addEventListener('click', (e) => { e.stopPropagation(); window.__lrShowInsertModule(); });
|
|
410
|
+
document.getElementById('lr-act-clear').addEventListener('click', (e) => { e.stopPropagation(); window.__lrClear(); });
|
|
411
|
+
document.getElementById('lr-module-cancel').addEventListener('click', (e) => { e.stopPropagation(); window.__lrCloseModuleDialog(); });
|
|
412
|
+
document.getElementById('lr-module-confirm').addEventListener('click', (e) => { e.stopPropagation(); window.__lrConfirmSaveModule(); });
|
|
413
|
+
document.getElementById('lr-btn-minimize').addEventListener('click', (e) => { e.stopPropagation(); window.__lrTogglePanel(); });
|
|
414
|
+
document.getElementById('lr-btn-maximize').addEventListener('click', (e) => { e.stopPropagation(); window.__lrToggleMaximize(); });
|
|
415
|
+
document.getElementById('lr-preview-copy').addEventListener('click', (e) => { e.stopPropagation(); window.__lrCopyPreview(); });
|
|
416
|
+
document.getElementById('lr-preview-close').addEventListener('click', (e) => { e.stopPropagation(); window.__lrClosePreview(); });
|
|
417
|
+
|
|
418
|
+
// Draggable con clamp (nunca sale del viewport)
|
|
419
|
+
let dragging = false, dragX = 0, dragY = 0;
|
|
420
|
+
document.getElementById('lr-panel-header').addEventListener('mousedown', (e) => {
|
|
421
|
+
dragging = true; dragX = e.clientX - panel.offsetLeft; dragY = e.clientY - panel.offsetTop; e.preventDefault();
|
|
422
|
+
});
|
|
423
|
+
document.addEventListener('mousemove', (e) => {
|
|
424
|
+
if (!dragging) return;
|
|
425
|
+
let newLeft = e.clientX - dragX;
|
|
426
|
+
let newTop = e.clientY - dragY;
|
|
427
|
+
// Clamp: header siempre visible dentro del viewport
|
|
428
|
+
newTop = Math.max(0, Math.min(newTop, window.innerHeight - 40));
|
|
429
|
+
newLeft = Math.max(-panel.offsetWidth + 80, Math.min(newLeft, window.innerWidth - 80));
|
|
430
|
+
panel.style.left = newLeft + 'px'; panel.style.right = 'auto'; panel.style.top = newTop + 'px';
|
|
431
|
+
});
|
|
432
|
+
document.addEventListener('mouseup', () => { dragging = false; });
|
|
433
|
+
|
|
434
|
+
// Doble-click en header → reset posición + expandir
|
|
435
|
+
document.getElementById('lr-panel-header').addEventListener('dblclick', (e) => {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
panel.style.top = '10px'; panel.style.right = '10px'; panel.style.left = 'auto';
|
|
438
|
+
if (panelMinimized) window.__lrTogglePanel();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ═══════════════════════════════════════════════════════════════
|
|
442
|
+
// LOCALIZADORES
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════
|
|
444
|
+
|
|
445
|
+
function getLocatorStrategies(el) {
|
|
446
|
+
const strategies = [];
|
|
447
|
+
const tag = el.tagName.toLowerCase();
|
|
448
|
+
|
|
449
|
+
// 1. data-automation-id
|
|
450
|
+
const autoId = el.getAttribute('data-automation-id');
|
|
451
|
+
if (autoId) {
|
|
452
|
+
strategies.push({ type: 'automation-id', selector: '[data-automation-id="' + autoId + '"]', playwright: "page.locator('[data-automation-id=\"" + autoId + "\"]')", reliability: 98, label: 'data-automation-id' });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 2. Ancestro con automation-id
|
|
456
|
+
if (!autoId) {
|
|
457
|
+
let parent = el.parentElement, depth = 0;
|
|
458
|
+
while (parent && depth < 5) {
|
|
459
|
+
const pAutoId = parent.getAttribute('data-automation-id');
|
|
460
|
+
if (pAutoId) {
|
|
461
|
+
const relSelector = tag + (el.className ? '.' + [...el.classList].filter(c => !c.startsWith('b-')).join('.') : '');
|
|
462
|
+
strategies.push({ type: 'parent-auto-id', selector: '[data-automation-id="' + pAutoId + '"] ' + relSelector, playwright: "page.locator('[data-automation-id=\"" + pAutoId + "\"]').locator('" + relSelector + "')", reliability: 85, label: 'parent auto-id' });
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
parent = parent.parentElement; depth++;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 3. title
|
|
470
|
+
const title = el.getAttribute('title');
|
|
471
|
+
if (title) {
|
|
472
|
+
strategies.push({ type: 'title', selector: tag + '[title="' + title + '"]', playwright: "page.locator('" + tag + "[title=\"" + title + "\"]')", reliability: 90, label: 'title' });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 4. role + name
|
|
476
|
+
const role = el.getAttribute('role') || (tag === 'button' ? 'button' : tag === 'input' ? 'textbox' : null);
|
|
477
|
+
const name = el.getAttribute('aria-label') || title || (tag === 'button' ? el.textContent.trim().substring(0, 50) : null);
|
|
478
|
+
if (role && name) {
|
|
479
|
+
strategies.push({ type: 'role', selector: 'role=' + role + '[name="' + name + '"]', playwright: "page.getByRole('" + role + "', { name: '" + name.replace(/'/g, "\\'") + "' })", reliability: 88, label: 'getByRole' });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 5. text
|
|
483
|
+
const text = el.textContent.trim();
|
|
484
|
+
if (text && text.length < 60 && (tag === 'button' || tag === 'a' || tag === 'span')) {
|
|
485
|
+
const shortText = text.length > 30 ? text.substring(0, 30) : text;
|
|
486
|
+
strategies.push({ type: 'text', selector: tag + ':has-text("' + shortText + '")', playwright: "page.locator('" + tag + ":has-text(\"" + shortText + "\")')", reliability: 75, label: 'text' });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 6. CSS classes
|
|
490
|
+
const sigClasses = [...el.classList].filter(c => !c.startsWith('b-') && c.length > 2);
|
|
491
|
+
if (sigClasses.length > 0) {
|
|
492
|
+
const cssSel = tag + '.' + sigClasses.join('.');
|
|
493
|
+
strategies.push({ type: 'css', selector: cssSel, playwright: "page.locator('" + cssSel + "')", reliability: 70, label: 'css' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 7. ID (estable)
|
|
497
|
+
const id = el.id;
|
|
498
|
+
if (id && !/[0-9a-f]{8}-[0-9a-f]{4}/.test(id) && !(/^[a-zA-Z0-9_-]{6,}$/.test(id) && !/^SDK|^TopBar|^whcm|^suite/.test(id))) {
|
|
499
|
+
strategies.push({ type: 'id', selector: '#' + id, playwright: "page.locator('#" + id + "')", reliability: 92, label: 'id' });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 8. Fallback
|
|
503
|
+
strategies.push({ type: 'fallback', selector: tag, playwright: "page.locator('" + tag + "')", reliability: 30, label: 'tag' });
|
|
504
|
+
|
|
505
|
+
strategies.sort((a, b) => b.reliability - a.reliability);
|
|
506
|
+
return strategies;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function getElementInfo(el) {
|
|
510
|
+
const tag = el.tagName.toLowerCase();
|
|
511
|
+
return {
|
|
512
|
+
tag, autoId: el.getAttribute('data-automation-id') || '',
|
|
513
|
+
title: el.getAttribute('title') || '',
|
|
514
|
+
text: (el.textContent || '').trim().substring(0, 80),
|
|
515
|
+
classes: [...el.classList].filter(c => !c.startsWith('b-')).join(' '),
|
|
516
|
+
isDisabled: el.disabled || el.classList.contains('dxbl-disabled') || el.getAttribute('aria-disabled') === 'true',
|
|
517
|
+
isVisible: el.offsetParent !== null && el.offsetWidth > 0 && el.offsetHeight > 0,
|
|
518
|
+
rect: el.getBoundingClientRect(),
|
|
519
|
+
inputType: el.getAttribute('type') || '',
|
|
520
|
+
value: (el.value || '').substring(0, 50),
|
|
521
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
522
|
+
role: el.getAttribute('role') || ''
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ═══════════════════════════════════════════════════════════════
|
|
527
|
+
// TOOLTIP (solo en modo inspect, no durante grabación)
|
|
528
|
+
// ═══════════════════════════════════════════════════════════════
|
|
529
|
+
let lastHoverEl = null;
|
|
530
|
+
|
|
531
|
+
document.addEventListener('mousemove', (e) => {
|
|
532
|
+
// Ocultar tooltip durante grabación para no estorbar
|
|
533
|
+
if (isRecording) { tooltip.style.display = 'none'; highlight.style.display = 'none'; return; }
|
|
534
|
+
if (e.target.closest('#lr-panel') || e.target.closest('#lr-tooltip')) {
|
|
535
|
+
tooltip.style.display = 'none'; highlight.style.display = 'none'; return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
539
|
+
if (!el || el === lastHoverEl) {
|
|
540
|
+
if (tooltip.style.display === 'block') {
|
|
541
|
+
const tx = e.clientX + 15, ty = e.clientY + 15;
|
|
542
|
+
tooltip.style.left = (tx + tooltip.offsetWidth > window.innerWidth ? e.clientX - tooltip.offsetWidth - 10 : tx) + 'px';
|
|
543
|
+
tooltip.style.top = (ty + tooltip.offsetHeight > window.innerHeight ? e.clientY - tooltip.offsetHeight - 10 : ty) + 'px';
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
lastHoverEl = el;
|
|
548
|
+
|
|
549
|
+
const info = getElementInfo(el);
|
|
550
|
+
const strategies = getLocatorStrategies(el);
|
|
551
|
+
const best = strategies[0];
|
|
552
|
+
|
|
553
|
+
highlight.style.display = 'block';
|
|
554
|
+
highlight.style.left = info.rect.left + 'px'; highlight.style.top = info.rect.top + 'px';
|
|
555
|
+
highlight.style.width = info.rect.width + 'px'; highlight.style.height = info.rect.height + 'px';
|
|
556
|
+
highlight.style.borderColor = info.isDisabled ? '#ff4757' : '#00ff88';
|
|
557
|
+
|
|
558
|
+
let html = '<div><span class="lr-label">Tag:</span> <span class="lr-value"><' + info.tag + '></span>';
|
|
559
|
+
if (info.inputType) html += ' <span class="lr-label">type=</span><span class="lr-value">' + info.inputType + '</span>';
|
|
560
|
+
html += '</div>';
|
|
561
|
+
if (info.autoId) html += '<div><span class="lr-label">automation-id:</span> <span class="lr-value lr-best">' + info.autoId + '</span></div>';
|
|
562
|
+
if (info.title) html += '<div><span class="lr-label">title:</span> <span class="lr-value">' + info.title + '</span></div>';
|
|
563
|
+
if (info.text && info.text !== info.title) html += '<div><span class="lr-label">text:</span> <span class="lr-value">' + info.text.substring(0, 60) + '</span></div>';
|
|
564
|
+
html += '<div style="margin-top:6px;border-top:1px solid #333;padding-top:4px;"><span class="lr-label">Mejor:</span> <span class="lr-value lr-best">' + best.selector + '</span> <span class="lr-label">(' + best.reliability + '%)</span></div>';
|
|
565
|
+
html += '<div style="color:#aaa;font-size:10px;margin-top:4px;">Ctrl+Click = capturar</div>';
|
|
566
|
+
|
|
567
|
+
tooltip.innerHTML = html;
|
|
568
|
+
tooltip.style.display = 'block';
|
|
569
|
+
const tx = e.clientX + 15, ty = e.clientY + 15;
|
|
570
|
+
tooltip.style.left = (tx + tooltip.offsetWidth > window.innerWidth ? e.clientX - tooltip.offsetWidth - 10 : tx) + 'px';
|
|
571
|
+
tooltip.style.top = (ty + tooltip.offsetHeight > window.innerHeight ? e.clientY - tooltip.offsetHeight - 10 : ty) + 'px';
|
|
572
|
+
}, true);
|
|
573
|
+
|
|
574
|
+
// ═══════════════════════════════════════════════════════════════
|
|
575
|
+
// CAPTURA (soporta actionType)
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════
|
|
577
|
+
|
|
578
|
+
function isRecorderUI(el) {
|
|
579
|
+
return el.closest('#lr-panel') || el.closest('#lr-tooltip') || el.closest('#lr-stop-btn') || el.closest('#lr-status')
|
|
580
|
+
// v4: la UI nueva tambien debe excluirse para no capturar pasos espurios al grabar
|
|
581
|
+
|| el.closest('#lr-context-menu') || el.closest('#lr-module-dialog') || el.closest('#lr-module-list');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function flashElement(el, color) {
|
|
585
|
+
const orig = el.style.outline;
|
|
586
|
+
const origOffset = el.style.outlineOffset;
|
|
587
|
+
el.style.outline = '3px solid ' + color;
|
|
588
|
+
el.style.outlineOffset = '2px';
|
|
589
|
+
setTimeout(() => { el.style.outline = orig; el.style.outlineOffset = origOffset; }, 800);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function captureElement(el, source, actionType, actionValue, assertionType, assertionValue) {
|
|
593
|
+
actionType = actionType || 'inspect';
|
|
594
|
+
actionValue = actionValue || null;
|
|
595
|
+
assertionType = assertionType || null;
|
|
596
|
+
assertionValue = assertionValue !== undefined ? assertionValue : null;
|
|
597
|
+
|
|
598
|
+
// Filtrar html/body — siempre falsos positivos
|
|
599
|
+
if (['HTML', 'BODY'].includes(el.tagName)) return false;
|
|
600
|
+
|
|
601
|
+
const info = getElementInfo(el);
|
|
602
|
+
const strategies = getLocatorStrategies(el);
|
|
603
|
+
const best = strategies[0];
|
|
604
|
+
const groupInput = document.getElementById('lr-group-input');
|
|
605
|
+
|
|
606
|
+
const capture = {
|
|
607
|
+
num: window.__capturedElements.length + 1,
|
|
608
|
+
timestamp: new Date().toISOString(),
|
|
609
|
+
source: source || 'manual',
|
|
610
|
+
actionType: actionType,
|
|
611
|
+
actionValue: actionValue,
|
|
612
|
+
assertionType: assertionType,
|
|
613
|
+
assertionValue: assertionValue,
|
|
614
|
+
group: (groupInput ? groupInput.value : '') || 'General',
|
|
615
|
+
url: window.location.pathname + window.location.search,
|
|
616
|
+
tag: info.tag, autoId: info.autoId, title: info.title,
|
|
617
|
+
text: info.text.substring(0, 80), classes: info.classes,
|
|
618
|
+
isDisabled: info.isDisabled, isVisible: info.isVisible,
|
|
619
|
+
inputType: info.inputType, value: info.value,
|
|
620
|
+
position: { x: Math.round(info.rect.x), y: Math.round(info.rect.y), w: Math.round(info.rect.width), h: Math.round(info.rect.height) },
|
|
621
|
+
bestLocator: best.selector, bestPlaywright: best.playwright,
|
|
622
|
+
reliability: best.reliability,
|
|
623
|
+
allStrategies: strategies.map(s => ({ type: s.type, selector: s.selector, playwright: s.playwright, reliability: s.reliability }))
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// Dedup solo para inspect (Ctrl+Click), no para recording (puede haber click + fill al mismo locator)
|
|
627
|
+
if (actionType === 'inspect') {
|
|
628
|
+
if (window.__capturedElements.some(c => c.bestLocator === capture.bestLocator)) {
|
|
629
|
+
showStatus('Duplicado ignorado');
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
window.__capturedElements.push(capture);
|
|
635
|
+
persistCaptures();
|
|
636
|
+
renderPanel();
|
|
637
|
+
|
|
638
|
+
const label = info.autoId || info.title || info.tag;
|
|
639
|
+
const actionLabel = actionType !== 'inspect' ? ' [' + actionType.toUpperCase() + ']' : '';
|
|
640
|
+
showStatus('#' + capture.num + actionLabel + ' ' + label + (actionValue ? ' → "' + actionValue + '"' : ''));
|
|
641
|
+
return capture;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ═══════════════════════════════════════════════════════════════
|
|
645
|
+
// CTRL+CLICK (modo inspect — funciona siempre)
|
|
646
|
+
// ═══════════════════════════════════════════════════════════════
|
|
647
|
+
|
|
648
|
+
document.addEventListener('click', (e) => {
|
|
649
|
+
if (isRecorderUI(e.target)) return;
|
|
650
|
+
|
|
651
|
+
if (e.ctrlKey) {
|
|
652
|
+
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
|
653
|
+
captureElement(e.target, 'ctrl-click', 'inspect', null);
|
|
654
|
+
flashElement(e.target, '#00ff88');
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (e.shiftKey) {
|
|
659
|
+
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
|
660
|
+
captureElement(e.target, 'shift-click', 'inspect', null);
|
|
661
|
+
e.target.querySelectorAll('button, input, select, textarea, [data-automation-id], [role="button"], [role="tab"]')
|
|
662
|
+
.forEach(child => captureElement(child, 'shift-child', 'inspect', null));
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}, true);
|
|
666
|
+
|
|
667
|
+
document.addEventListener('keydown', (e) => {
|
|
668
|
+
if (e.key === 'Escape') {
|
|
669
|
+
// v4: si hay menus/dialogos abiertos, Escape los cierra primero (no minimiza el panel)
|
|
670
|
+
if (contextMenu.style.display === 'block') { e.preventDefault(); hideContextMenu(); return; }
|
|
671
|
+
if (moduleDialog.style.display === 'block') { e.preventDefault(); window.__lrCloseModuleDialog(); return; }
|
|
672
|
+
if (moduleList.style.display === 'block') { e.preventDefault(); moduleList.style.display = 'none'; return; }
|
|
673
|
+
e.preventDefault(); window.__lrTogglePanel();
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// ═══════════════════════════════════════════════════════════════
|
|
678
|
+
// v4: MEJORA 1 — ASSERTIONS via CLIC DERECHO (solo en modo grabar)
|
|
679
|
+
// ═══════════════════════════════════════════════════════════════
|
|
680
|
+
|
|
681
|
+
function hideContextMenu() { contextMenu.style.display = 'none'; }
|
|
682
|
+
|
|
683
|
+
function showContextMenu(el, x, y) {
|
|
684
|
+
const info = getElementInfo(el);
|
|
685
|
+
const isInputLike = ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName);
|
|
686
|
+
const text = (el.textContent || '').trim().substring(0, 40);
|
|
687
|
+
const value = (el.value || '').substring(0, 40);
|
|
688
|
+
|
|
689
|
+
const items = [];
|
|
690
|
+
items.push({
|
|
691
|
+
type: 'text',
|
|
692
|
+
value: info.text || text,
|
|
693
|
+
label: 'Assert: texto visible = <span class="lr-ctx-val">"' + (info.text || text || '(vacio)') + '"</span>'
|
|
694
|
+
});
|
|
695
|
+
items.push({
|
|
696
|
+
type: 'exists',
|
|
697
|
+
value: null,
|
|
698
|
+
label: 'Assert: elemento existe / visible'
|
|
699
|
+
});
|
|
700
|
+
if (isInputLike) {
|
|
701
|
+
items.push({
|
|
702
|
+
type: 'value',
|
|
703
|
+
value: value,
|
|
704
|
+
label: 'Assert: valor = <span class="lr-ctx-val">"' + (value || '(vacio)') + '"</span>'
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let html = '<div class="lr-ctx-title">Assertions</div>';
|
|
709
|
+
items.forEach((it, i) => {
|
|
710
|
+
html += '<div class="lr-ctx-item" data-ctx-idx="' + i + '">' + it.label + '</div>';
|
|
711
|
+
});
|
|
712
|
+
html += '<div class="lr-ctx-item lr-ctx-cancel" data-ctx-cancel="1">Cancelar</div>';
|
|
713
|
+
contextMenu.innerHTML = html;
|
|
714
|
+
|
|
715
|
+
// Posicion (clamp dentro del viewport)
|
|
716
|
+
contextMenu.style.display = 'block';
|
|
717
|
+
const mw = contextMenu.offsetWidth, mh = contextMenu.offsetHeight;
|
|
718
|
+
contextMenu.style.left = Math.min(x, window.innerWidth - mw - 8) + 'px';
|
|
719
|
+
contextMenu.style.top = Math.min(y, window.innerHeight - mh - 8) + 'px';
|
|
720
|
+
|
|
721
|
+
contextMenu.querySelectorAll('.lr-ctx-item').forEach(node => {
|
|
722
|
+
node.addEventListener('click', (ev) => {
|
|
723
|
+
ev.stopPropagation(); ev.preventDefault();
|
|
724
|
+
if (node.getAttribute('data-ctx-cancel')) { hideContextMenu(); return; }
|
|
725
|
+
const it = items[parseInt(node.getAttribute('data-ctx-idx'), 10)];
|
|
726
|
+
captureElement(el, 'rec-assert', 'assert', null, it.type, it.value);
|
|
727
|
+
flashElement(el, '#10b981');
|
|
728
|
+
hideContextMenu();
|
|
729
|
+
}, true);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
document.addEventListener('contextmenu', (e) => {
|
|
734
|
+
if (!isRecording) return; // solo activo grabando
|
|
735
|
+
if (isRecorderUI(e.target)) return; // no sobre la UI del grabador
|
|
736
|
+
if (['HTML', 'BODY'].includes(e.target.tagName)) return;
|
|
737
|
+
e.preventDefault(); e.stopPropagation();
|
|
738
|
+
showContextMenu(e.target, e.clientX, e.clientY);
|
|
739
|
+
}, true);
|
|
740
|
+
|
|
741
|
+
// Cerrar menu contextual al hacer click fuera
|
|
742
|
+
document.addEventListener('click', (e) => {
|
|
743
|
+
if (contextMenu.style.display === 'block' && !contextMenu.contains(e.target)) hideContextMenu();
|
|
744
|
+
if (moduleList.style.display === 'block' && !moduleList.contains(e.target) && e.target.id !== 'lr-act-insert-module') moduleList.style.display = 'none';
|
|
745
|
+
}, true);
|
|
746
|
+
|
|
747
|
+
// ═══════════════════════════════════════════════════════════════
|
|
748
|
+
// MODO GRABAR (captura automática de clicks, fills, selects)
|
|
749
|
+
// ═══════════════════════════════════════════════════════════════
|
|
750
|
+
|
|
751
|
+
function onRecordClick(e) {
|
|
752
|
+
if (!isRecording) return;
|
|
753
|
+
const el = e.target;
|
|
754
|
+
if (isRecorderUI(el)) return;
|
|
755
|
+
// No capturar inputs/textareas como click (se capturan como fill)
|
|
756
|
+
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName)) return;
|
|
757
|
+
// No capturar html/body (falsos positivos)
|
|
758
|
+
if (['HTML', 'BODY'].includes(el.tagName)) return;
|
|
759
|
+
|
|
760
|
+
const strategies = getLocatorStrategies(el);
|
|
761
|
+
const selector = strategies[0].selector;
|
|
762
|
+
const now = Date.now();
|
|
763
|
+
|
|
764
|
+
// Dedup: mismo selector en 400ms
|
|
765
|
+
if (selector === _lastRecSelector && (now - _lastRecTime) < 400) return;
|
|
766
|
+
|
|
767
|
+
// Dedup padre-hijo: si el click anterior fue en un hijo/padre del actual (burbujeo), skip
|
|
768
|
+
if (_lastRecElement && (now - _lastRecTime) < 100) {
|
|
769
|
+
if (el.contains(_lastRecElement) || _lastRecElement.contains(el)) return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
_lastRecSelector = selector;
|
|
773
|
+
_lastRecTime = now;
|
|
774
|
+
_lastRecElement = el;
|
|
775
|
+
|
|
776
|
+
// NO preventDefault — el click pasa normalmente a la app
|
|
777
|
+
captureElement(el, 'rec-click', 'click', null);
|
|
778
|
+
flashElement(el, '#f97316');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function onRecordInput(e) {
|
|
782
|
+
if (!isRecording) return;
|
|
783
|
+
const el = e.target;
|
|
784
|
+
if (!['INPUT', 'TEXTAREA'].includes(el.tagName)) return;
|
|
785
|
+
if (isRecorderUI(el)) return;
|
|
786
|
+
|
|
787
|
+
// Debounce 800ms — capturar valor final
|
|
788
|
+
if (el.__lrDebounce) clearTimeout(el.__lrDebounce);
|
|
789
|
+
el.__lrDebounce = setTimeout(() => {
|
|
790
|
+
const finalValue = el.value;
|
|
791
|
+
if (!finalValue) return;
|
|
792
|
+
|
|
793
|
+
const strategies = getLocatorStrategies(el);
|
|
794
|
+
const bestSelector = strategies[0].selector;
|
|
795
|
+
|
|
796
|
+
// Actualizar fill existente para el mismo locator
|
|
797
|
+
const existing = [...window.__capturedElements].reverse().find(
|
|
798
|
+
c => c.actionType === 'fill' && c.bestLocator === bestSelector
|
|
799
|
+
);
|
|
800
|
+
if (existing) {
|
|
801
|
+
existing.actionValue = finalValue;
|
|
802
|
+
existing.value = finalValue;
|
|
803
|
+
existing.timestamp = new Date().toISOString();
|
|
804
|
+
persistCaptures();
|
|
805
|
+
renderPanel();
|
|
806
|
+
showStatus('Fill actualizado: "' + finalValue + '"');
|
|
807
|
+
} else {
|
|
808
|
+
captureElement(el, 'rec-fill', 'fill', finalValue);
|
|
809
|
+
flashElement(el, '#3b82f6');
|
|
810
|
+
}
|
|
811
|
+
}, 800);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function onRecordChange(e) {
|
|
815
|
+
if (!isRecording) return;
|
|
816
|
+
const el = e.target;
|
|
817
|
+
if (isRecorderUI(el)) return;
|
|
818
|
+
if (el.tagName === 'SELECT') {
|
|
819
|
+
captureElement(el, 'rec-select', 'select', el.value);
|
|
820
|
+
flashElement(el, '#a78bfa');
|
|
821
|
+
}
|
|
822
|
+
// Checkboxes y radio buttons como click
|
|
823
|
+
if (el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')) {
|
|
824
|
+
captureElement(el, 'rec-check', 'click', null);
|
|
825
|
+
flashElement(el, '#f97316');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function createFloatingStopButton() {
|
|
830
|
+
if (document.getElementById('lr-stop-btn')) return;
|
|
831
|
+
const btn = document.createElement('div');
|
|
832
|
+
btn.id = 'lr-stop-btn';
|
|
833
|
+
btn.innerHTML = '\u23F9 <b style="font-family:monospace;font-size:11px;letter-spacing:1px">DETENER REC</b>';
|
|
834
|
+
btn.addEventListener('click', (e) => {
|
|
835
|
+
e.stopPropagation(); e.preventDefault();
|
|
836
|
+
window.__lrStopRec();
|
|
837
|
+
});
|
|
838
|
+
document.body.appendChild(btn);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function removeFloatingStopButton() {
|
|
842
|
+
const btn = document.getElementById('lr-stop-btn');
|
|
843
|
+
if (btn) btn.remove();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function updateRecordingUI() {
|
|
847
|
+
const recBtn = document.getElementById('lr-btn-record');
|
|
848
|
+
const stopBtn = document.getElementById('lr-btn-stop');
|
|
849
|
+
const recStatus = document.getElementById('lr-rec-status');
|
|
850
|
+
|
|
851
|
+
if (recBtn) recBtn.disabled = isRecording;
|
|
852
|
+
if (stopBtn) stopBtn.disabled = !isRecording;
|
|
853
|
+
panel.style.borderColor = isRecording ? '#ef4444' : '#00b894';
|
|
854
|
+
|
|
855
|
+
if (recStatus) {
|
|
856
|
+
if (isRecording) {
|
|
857
|
+
recStatus.textContent = '\u25CF REC';
|
|
858
|
+
recStatus.className = 'lr-rec-active';
|
|
859
|
+
} else {
|
|
860
|
+
recStatus.textContent = window.__capturedElements.length > 0
|
|
861
|
+
? window.__capturedElements.length + ' PASOS'
|
|
862
|
+
: 'LISTO';
|
|
863
|
+
recStatus.className = 'lr-rec-idle';
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
window.__lrStartRec = function(silent) {
|
|
869
|
+
isRecording = true;
|
|
870
|
+
persistRecState();
|
|
871
|
+
document.addEventListener('click', onRecordClick, true);
|
|
872
|
+
document.addEventListener('input', onRecordInput, true);
|
|
873
|
+
document.addEventListener('change', onRecordChange, true);
|
|
874
|
+
createFloatingStopButton();
|
|
875
|
+
updateRecordingUI();
|
|
876
|
+
if (!silent) showStatus('Grabaci\u00f3n iniciada \u2014 interact\u00faa normalmente, todo se captura');
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
window.__lrStopRec = function() {
|
|
880
|
+
isRecording = false;
|
|
881
|
+
persistRecState();
|
|
882
|
+
document.removeEventListener('click', onRecordClick, true);
|
|
883
|
+
document.removeEventListener('input', onRecordInput, true);
|
|
884
|
+
document.removeEventListener('change', onRecordChange, true);
|
|
885
|
+
removeFloatingStopButton();
|
|
886
|
+
hideContextMenu(); // v4: el menu de assertions solo aplica grabando
|
|
887
|
+
updateRecordingUI();
|
|
888
|
+
showStatus('Grabaci\u00f3n detenida \u2014 ' + window.__capturedElements.length + ' pasos capturados');
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
// ═══════════════════════════════════════════════════════════════
|
|
892
|
+
// PANEL RENDER
|
|
893
|
+
// ═══════════════════════════════════════════════════════════════
|
|
894
|
+
|
|
895
|
+
function renderPanel() {
|
|
896
|
+
const list = document.getElementById('lr-panel-list');
|
|
897
|
+
const count = document.getElementById('lr-count');
|
|
898
|
+
const groupInfo = document.getElementById('lr-group-info');
|
|
899
|
+
count.textContent = window.__capturedElements.length;
|
|
900
|
+
updateRecordingUI();
|
|
901
|
+
|
|
902
|
+
if (window.__capturedElements.length === 0) {
|
|
903
|
+
list.innerHTML = '<div style="color:#888;text-align:center;padding:20px;">Presiona Grabar o usa Ctrl+Click</div>';
|
|
904
|
+
if (groupInfo) groupInfo.textContent = '';
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const groups = {};
|
|
909
|
+
window.__capturedElements.forEach(cap => {
|
|
910
|
+
const g = cap.group || 'General';
|
|
911
|
+
if (!groups[g]) groups[g] = [];
|
|
912
|
+
groups[g].push(cap);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
const groupNames = Object.keys(groups);
|
|
916
|
+
if (groupInfo) groupInfo.textContent = groupNames.length + ' grupo' + (groupNames.length !== 1 ? 's' : '');
|
|
917
|
+
|
|
918
|
+
let html = '';
|
|
919
|
+
groupNames.forEach(groupName => {
|
|
920
|
+
html += '<div class="lr-group-header"><span>' + groupName + '</span><span class="lr-group-count">' + groups[groupName].length + ' elem.</span></div>';
|
|
921
|
+
|
|
922
|
+
groups[groupName].forEach(cap => {
|
|
923
|
+
const idx = window.__capturedElements.indexOf(cap);
|
|
924
|
+
const actionClass = cap.actionType === 'click' ? ' lr-item-click'
|
|
925
|
+
: cap.actionType === 'fill' ? ' lr-item-fill'
|
|
926
|
+
: cap.actionType === 'select' ? ' lr-item-select'
|
|
927
|
+
: cap.actionType === 'assert' ? ' lr-item-assert'
|
|
928
|
+
: cap.actionType === 'screenshot' ? ' lr-item-screenshot'
|
|
929
|
+
: cap.isDisabled ? ' lr-item-disabled' : '';
|
|
930
|
+
|
|
931
|
+
html += '<div class="lr-item' + actionClass + '">';
|
|
932
|
+
html += '<button class="lr-item-delete" onclick="window.__lrDeleteOne(' + idx + ')" title="Eliminar">\u00d7</button>';
|
|
933
|
+
html += '<button class="lr-item-copy" onclick="window.__lrCopyOne(' + idx + ')">Copiar</button>';
|
|
934
|
+
html += '<div class="lr-item-num">#' + cap.num;
|
|
935
|
+
|
|
936
|
+
// Action badge
|
|
937
|
+
if (cap.actionType && cap.actionType !== 'inspect') {
|
|
938
|
+
const badgeClass = 'lr-action-' + cap.actionType;
|
|
939
|
+
html += ' <span class="lr-action-badge ' + badgeClass + '">' + cap.actionType.toUpperCase() + '</span>';
|
|
940
|
+
}
|
|
941
|
+
html += '</div>';
|
|
942
|
+
|
|
943
|
+
html += '<div class="lr-item-tag"><' + cap.tag + '>';
|
|
944
|
+
if (cap.autoId) html += ' <span style="color:#ffd700;">' + cap.autoId + '</span>';
|
|
945
|
+
if (cap.title) html += ' title="' + cap.title + '"';
|
|
946
|
+
if (cap.isDisabled) html += ' <span style="color:#ff4757;">[DISABLED]</span>';
|
|
947
|
+
html += '</div>';
|
|
948
|
+
html += '<div class="lr-item-loc">' + cap.bestPlaywright + '</div>';
|
|
949
|
+
|
|
950
|
+
// Valor de la acción
|
|
951
|
+
if (cap.actionValue) {
|
|
952
|
+
html += '<div class="lr-item-value">\u2192 "' + cap.actionValue + '"</div>';
|
|
953
|
+
}
|
|
954
|
+
// v4: detalle de assertion (verde)
|
|
955
|
+
if (cap.actionType === 'assert') {
|
|
956
|
+
let assertTxt;
|
|
957
|
+
if (cap.assertionType === 'text') assertTxt = 'assert texto = "' + (cap.assertionValue || '') + '"';
|
|
958
|
+
else if (cap.assertionType === 'value') assertTxt = 'assert valor = "' + (cap.assertionValue || '') + '"';
|
|
959
|
+
else assertTxt = 'assert visible / existe';
|
|
960
|
+
html += '<div class="lr-item-assert-detail">✓ ' + assertTxt + '</div>';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// v4: detalle de screenshot (amber)
|
|
964
|
+
if (cap.actionType === 'screenshot') {
|
|
965
|
+
html += '<div class="lr-item-screenshot-detail">\u{1F4F7} ' + (cap.screenshotFile || 'screenshot') + '</div>';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (cap.text && cap.text !== cap.title && cap.text !== cap.autoId && !cap.actionValue && cap.actionType !== 'assert' && cap.actionType !== 'screenshot') {
|
|
969
|
+
html += '<div class="lr-item-extra">text: "' + cap.text.substring(0, 40) + '"</div>';
|
|
970
|
+
}
|
|
971
|
+
html += '<div class="lr-item-extra">' + cap.reliability + '% | ' + cap.position.x + ',' + cap.position.y + '</div>';
|
|
972
|
+
html += '</div>';
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
list.innerHTML = html;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// ═══════════════════════════════════════════════════════════════
|
|
980
|
+
// ACCIONES
|
|
981
|
+
// ═══════════════════════════════════════════════════════════════
|
|
982
|
+
|
|
983
|
+
function showStatus(msg) {
|
|
984
|
+
statusBar.textContent = msg;
|
|
985
|
+
statusBar.style.opacity = '1';
|
|
986
|
+
setTimeout(() => { statusBar.style.opacity = '0'; }, 2500);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function showPreview(title, content) {
|
|
990
|
+
try {
|
|
991
|
+
const container = document.getElementById('lr-preview-container');
|
|
992
|
+
document.getElementById('lr-preview-title').textContent = title;
|
|
993
|
+
document.getElementById('lr-preview-text').value = content;
|
|
994
|
+
container.style.display = 'block';
|
|
995
|
+
showStatus(title + ' generado — click "Copiar" para copiar al clipboard');
|
|
996
|
+
} catch (e) {
|
|
997
|
+
console.error('[LR] showPreview error:', e);
|
|
998
|
+
showStatus('Error mostrando preview: ' + (e.message || e));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
window.__lrCopyPreview = function() {
|
|
1003
|
+
const ta = document.getElementById('lr-preview-text');
|
|
1004
|
+
navigator.clipboard.writeText(ta.value).then(() => showStatus('Copiado')).catch(() => { ta.select(); document.execCommand('copy'); showStatus('Copiado (fallback)'); });
|
|
1005
|
+
};
|
|
1006
|
+
window.__lrClosePreview = function() { document.getElementById('lr-preview-container').style.display = 'none'; };
|
|
1007
|
+
|
|
1008
|
+
window.__lrDetectVisible = function() {
|
|
1009
|
+
let added = 0;
|
|
1010
|
+
document.querySelectorAll('[data-automation-id]').forEach(el => {
|
|
1011
|
+
if (el.offsetParent !== null && el.offsetWidth > 0) { if (captureElement(el, 'detect-visible', 'inspect', null)) added++; }
|
|
1012
|
+
});
|
|
1013
|
+
showStatus('Detectados ' + added + ' elementos visibles');
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
window.__lrClear = function() {
|
|
1017
|
+
if (isRecording) window.__lrStopRec();
|
|
1018
|
+
window.__capturedElements = [];
|
|
1019
|
+
persistCaptures();
|
|
1020
|
+
renderPanel();
|
|
1021
|
+
document.getElementById('lr-preview-container').style.display = 'none';
|
|
1022
|
+
hideContextMenu(); // v4
|
|
1023
|
+
moduleDialog.style.display = 'none'; // v4
|
|
1024
|
+
moduleList.style.display = 'none'; // v4
|
|
1025
|
+
showStatus('Panel limpiado');
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
window.__lrDeleteOne = function(index) {
|
|
1029
|
+
const cap = window.__capturedElements[index];
|
|
1030
|
+
if (!cap) return;
|
|
1031
|
+
window.__capturedElements.splice(index, 1);
|
|
1032
|
+
window.__capturedElements.forEach((c, i) => { c.num = i + 1; });
|
|
1033
|
+
persistCaptures(); renderPanel();
|
|
1034
|
+
showStatus('Eliminado #' + (index + 1));
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
window.__lrCopyOne = function(index) {
|
|
1038
|
+
const cap = window.__capturedElements[index];
|
|
1039
|
+
if (cap) navigator.clipboard.writeText(cap.bestPlaywright).then(() => showStatus('Copiado'));
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
window.__lrCopyAll = function() {
|
|
1043
|
+
if (window.__capturedElements.length === 0) { showStatus('Vac\u00edo'); return; }
|
|
1044
|
+
const groups = {};
|
|
1045
|
+
window.__capturedElements.forEach(c => { const g = c.group || 'General'; if (!groups[g]) groups[g] = []; groups[g].push(c); });
|
|
1046
|
+
let text = '';
|
|
1047
|
+
Object.keys(groups).forEach(gn => {
|
|
1048
|
+
text += '// \u2500\u2500 ' + gn + ' \u2500\u2500\n';
|
|
1049
|
+
groups[gn].forEach(c => {
|
|
1050
|
+
const action = c.actionType !== 'inspect' ? ' [' + c.actionType + ']' : '';
|
|
1051
|
+
text += '// #' + c.num + action + ' ' + c.tag + (c.autoId ? ' [' + c.autoId + ']' : '') + '\n';
|
|
1052
|
+
text += c.bestPlaywright + (c.actionValue ? ' // → "' + c.actionValue + '"' : '') + '\n\n';
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
navigator.clipboard.writeText(text).then(() => showStatus('Copiados ' + window.__capturedElements.length + ' pasos'));
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// Helper genérico para exportar — evita duplicar try-catch en cada formato
|
|
1059
|
+
async function exportFormat(name, generatorFn, fallbackContent) {
|
|
1060
|
+
if (window.__capturedElements.length === 0) { showStatus('No hay pasos'); return; }
|
|
1061
|
+
const json = JSON.stringify(window.__capturedElements);
|
|
1062
|
+
console.log('[LR] Exportando ' + name + '... (' + window.__capturedElements.length + ' pasos)');
|
|
1063
|
+
if (generatorFn) {
|
|
1064
|
+
try {
|
|
1065
|
+
const r = await generatorFn(json);
|
|
1066
|
+
console.log('[LR] ' + name + ' generado: ' + r.filename);
|
|
1067
|
+
showPreview(name + ': ' + r.filename, r.content);
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
console.error('[LR] Error ' + name + ':', e);
|
|
1070
|
+
showStatus('Error ' + name + ': ' + (e.message || e));
|
|
1071
|
+
}
|
|
1072
|
+
} else {
|
|
1073
|
+
showPreview(name + ' (fallback)', fallbackContent);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
window.__lrExportJSON = async function() {
|
|
1078
|
+
const json = JSON.stringify(window.__capturedElements, null, 2);
|
|
1079
|
+
console.log('[LR] Exportando JSON...');
|
|
1080
|
+
if (window.__lrSaveJSON) {
|
|
1081
|
+
try { const r = await window.__lrSaveJSON(json); showPreview('JSON: ' + r.filename, json); } catch (e) { console.error('[LR]', e); showPreview('JSON', json); }
|
|
1082
|
+
} else {
|
|
1083
|
+
showPreview('JSON', json);
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
window.__lrExportTS = function() { exportFormat('Playwright TS', window.__lrGenerateTS, '// Sin función de generación'); };
|
|
1088
|
+
window.__lrExportRobot = function() { exportFormat('Robot Selenium', window.__lrGenerateRobot, '// Sin función de generación'); };
|
|
1089
|
+
window.__lrExportPwPy = function() { exportFormat('Playwright PY', window.__lrGeneratePwPy, '# Sin función de generación'); };
|
|
1090
|
+
window.__lrExportCypress = function() { exportFormat('Cypress', window.__lrGenerateCypress, '// Sin función de generación'); };
|
|
1091
|
+
window.__lrExportGherkin = function() { exportFormat('Gherkin', window.__lrGenerateGherkin, '# Sin función de generación'); };
|
|
1092
|
+
window.__lrExportCSV = function() { exportFormat('CSV', window.__lrGenerateCSV, '# Sin función de generación'); };
|
|
1093
|
+
|
|
1094
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1095
|
+
// v4: MEJORA 2 — SCREENSHOTS / VISUAL BASELINE
|
|
1096
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1097
|
+
window.__lrTakeScreenshot = async function() {
|
|
1098
|
+
if (!window.__lrCaptureScreenshot) {
|
|
1099
|
+
showStatus('Screenshot no disponible (funcion no expuesta)');
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const stepNum = window.__capturedElements.length + 1;
|
|
1103
|
+
showStatus('Capturando screenshot...');
|
|
1104
|
+
// v4: ocultar la UI del grabador para no contaminar el baseline visual
|
|
1105
|
+
const uiEls = ['lr-panel', 'lr-status', 'lr-stop-btn', 'lr-tooltip', 'lr-highlight', 'lr-context-menu']
|
|
1106
|
+
.map(id => document.getElementById(id)).filter(Boolean);
|
|
1107
|
+
const prevDisplay = uiEls.map(el => el.style.display);
|
|
1108
|
+
uiEls.forEach(el => { el.style.display = 'none'; });
|
|
1109
|
+
try {
|
|
1110
|
+
const r = await window.__lrCaptureScreenshot(stepNum);
|
|
1111
|
+
uiEls.forEach((el, i) => { el.style.display = prevDisplay[i]; });
|
|
1112
|
+
const groupInput = document.getElementById('lr-group-input');
|
|
1113
|
+
const capture = {
|
|
1114
|
+
num: stepNum,
|
|
1115
|
+
timestamp: new Date().toISOString(),
|
|
1116
|
+
source: 'screenshot-btn',
|
|
1117
|
+
actionType: 'screenshot',
|
|
1118
|
+
actionValue: null,
|
|
1119
|
+
assertionType: null,
|
|
1120
|
+
assertionValue: null,
|
|
1121
|
+
screenshotFile: (r && r.filename) || ('step-' + stepNum + '.png'),
|
|
1122
|
+
group: (groupInput ? groupInput.value : '') || 'General',
|
|
1123
|
+
url: window.location.pathname + window.location.search,
|
|
1124
|
+
tag: 'page', autoId: '', title: '', text: '', classes: '',
|
|
1125
|
+
isDisabled: false, isVisible: true, inputType: '', value: '',
|
|
1126
|
+
position: { x: 0, y: 0, w: 0, h: 0 },
|
|
1127
|
+
bestLocator: 'page', bestPlaywright: 'page',
|
|
1128
|
+
reliability: 100, allStrategies: []
|
|
1129
|
+
};
|
|
1130
|
+
window.__capturedElements.push(capture);
|
|
1131
|
+
persistCaptures();
|
|
1132
|
+
renderPanel();
|
|
1133
|
+
showStatus('Screenshot #' + stepNum + ' → ' + capture.screenshotFile);
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
uiEls.forEach((el, i) => { el.style.display = prevDisplay[i]; }); // restaurar UI si fallo
|
|
1136
|
+
console.error('[LR] Error screenshot:', e);
|
|
1137
|
+
showStatus('Error screenshot: ' + (e.message || e));
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1142
|
+
// v4: MEJORA 3 — MODULOS REUTILIZABLES
|
|
1143
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1144
|
+
window.__lrCloseModuleDialog = function() {
|
|
1145
|
+
moduleDialog.style.display = 'none';
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
window.__lrShowSaveModule = function() {
|
|
1149
|
+
if (window.__capturedElements.length === 0) { showStatus('No hay capturas para guardar como modulo'); return; }
|
|
1150
|
+
if (!window.__lrSaveModule) { showStatus('Modulos no disponibles (funcion no expuesta)'); return; }
|
|
1151
|
+
document.getElementById('lr-module-name').value = '';
|
|
1152
|
+
document.getElementById('lr-module-desc').value = '';
|
|
1153
|
+
moduleList.style.display = 'none';
|
|
1154
|
+
moduleDialog.style.display = 'block';
|
|
1155
|
+
document.getElementById('lr-module-name').focus();
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
window.__lrConfirmSaveModule = async function() {
|
|
1159
|
+
const nameRaw = (document.getElementById('lr-module-name').value || '').trim();
|
|
1160
|
+
const desc = (document.getElementById('lr-module-desc').value || '').trim();
|
|
1161
|
+
if (!nameRaw) { showStatus('Indica un nombre para el modulo'); return; }
|
|
1162
|
+
const name = nameRaw.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-');
|
|
1163
|
+
const payload = {
|
|
1164
|
+
name: name,
|
|
1165
|
+
displayName: nameRaw,
|
|
1166
|
+
description: desc,
|
|
1167
|
+
stepCount: window.__capturedElements.length,
|
|
1168
|
+
savedAt: new Date().toISOString(),
|
|
1169
|
+
captures: window.__capturedElements
|
|
1170
|
+
};
|
|
1171
|
+
try {
|
|
1172
|
+
const r = await window.__lrSaveModule(JSON.stringify(payload));
|
|
1173
|
+
moduleDialog.style.display = 'none';
|
|
1174
|
+
showStatus('Modulo guardado: ' + ((r && r.filename) || (name + '.json')));
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
console.error('[LR] Error guardando modulo:', e);
|
|
1177
|
+
showStatus('Error guardando modulo: ' + (e.message || e));
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
window.__lrShowInsertModule = async function() {
|
|
1182
|
+
if (!window.__lrListModules) { showStatus('Modulos no disponibles (funcion no expuesta)'); return; }
|
|
1183
|
+
moduleDialog.style.display = 'none';
|
|
1184
|
+
let modules = [];
|
|
1185
|
+
try {
|
|
1186
|
+
const raw = await window.__lrListModules();
|
|
1187
|
+
modules = JSON.parse(raw || '[]');
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
console.error('[LR] Error listando modulos:', e);
|
|
1190
|
+
showStatus('Error listando modulos: ' + (e.message || e));
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
let html = '<div class="lr-modlist-title">Insertar modulo</div>';
|
|
1195
|
+
if (!modules.length) {
|
|
1196
|
+
html += '<div class="lr-modlist-empty">No hay modulos guardados todavia</div>';
|
|
1197
|
+
} else {
|
|
1198
|
+
modules.forEach((m, i) => {
|
|
1199
|
+
html += '<div class="lr-modlist-item" data-mod-idx="' + i + '" data-mod-name="' + m.name + '">'
|
|
1200
|
+
+ (m.displayName || m.name)
|
|
1201
|
+
+ '<div class="lr-modlist-meta">' + (m.stepCount || 0) + ' pasos'
|
|
1202
|
+
+ (m.description ? ' — ' + m.description : '') + '</div></div>';
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
moduleList.innerHTML = html;
|
|
1206
|
+
|
|
1207
|
+
// Posicionar bajo el boton "Insertar modulo"
|
|
1208
|
+
const btn = document.getElementById('lr-act-insert-module');
|
|
1209
|
+
const rect = btn.getBoundingClientRect();
|
|
1210
|
+
moduleList.style.display = 'block';
|
|
1211
|
+
const mw = moduleList.offsetWidth;
|
|
1212
|
+
moduleList.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - mw - 8)) + 'px';
|
|
1213
|
+
moduleList.style.top = Math.min(rect.bottom + 4, window.innerHeight - moduleList.offsetHeight - 8) + 'px';
|
|
1214
|
+
|
|
1215
|
+
moduleList.querySelectorAll('.lr-modlist-item').forEach(node => {
|
|
1216
|
+
node.addEventListener('click', (ev) => {
|
|
1217
|
+
ev.stopPropagation(); ev.preventDefault();
|
|
1218
|
+
window.__lrInsertModule(node.getAttribute('data-mod-name'));
|
|
1219
|
+
}, true);
|
|
1220
|
+
});
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
window.__lrInsertModule = async function(name) {
|
|
1224
|
+
moduleList.style.display = 'none';
|
|
1225
|
+
if (!window.__lrLoadModule) { showStatus('Modulos no disponibles'); return; }
|
|
1226
|
+
try {
|
|
1227
|
+
const raw = await window.__lrLoadModule(name);
|
|
1228
|
+
const mod = JSON.parse(raw);
|
|
1229
|
+
const modCaptures = (mod.captures || []).map(c => Object.assign({}, c, { source: 'module:' + name }));
|
|
1230
|
+
// Prepone las capturas del modulo y re-numera todo
|
|
1231
|
+
window.__capturedElements = modCaptures.concat(window.__capturedElements);
|
|
1232
|
+
window.__capturedElements.forEach((c, i) => { c.num = i + 1; });
|
|
1233
|
+
persistCaptures();
|
|
1234
|
+
renderPanel();
|
|
1235
|
+
showStatus('Modulo "' + (mod.displayName || name) + '" insertado (' + modCaptures.length + ' pasos)');
|
|
1236
|
+
} catch (e) {
|
|
1237
|
+
console.error('[LR] Error insertando modulo:', e);
|
|
1238
|
+
showStatus('Error insertando modulo: ' + (e.message || e));
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
let panelMinimized = false;
|
|
1243
|
+
let panelMaximized = false;
|
|
1244
|
+
const PANEL_DEFAULT_WIDTH = '440px';
|
|
1245
|
+
const PANEL_MIN_WIDTH = '180px';
|
|
1246
|
+
|
|
1247
|
+
window.__lrTogglePanel = function() {
|
|
1248
|
+
if (panelMaximized) { window.__lrToggleMaximize(); return; }
|
|
1249
|
+
panelMinimized = !panelMinimized;
|
|
1250
|
+
['lr-panel-list', 'lr-panel-actions', 'lr-panel-footer', 'lr-panel-group'].forEach(id => {
|
|
1251
|
+
const el = document.getElementById(id);
|
|
1252
|
+
if (el) el.style.display = panelMinimized ? 'none' : '';
|
|
1253
|
+
});
|
|
1254
|
+
const preview = document.getElementById('lr-preview-container');
|
|
1255
|
+
if (preview && panelMinimized) preview.style.display = 'none';
|
|
1256
|
+
panel.style.width = panelMinimized ? PANEL_MIN_WIDTH : PANEL_DEFAULT_WIDTH;
|
|
1257
|
+
panel.style.maxHeight = panelMinimized ? 'auto' : '90vh';
|
|
1258
|
+
// Actualizar icono del botón minimizar
|
|
1259
|
+
const btnMin = document.getElementById('lr-btn-minimize');
|
|
1260
|
+
if (btnMin) btnMin.innerHTML = panelMinimized ? '+' : '−';
|
|
1261
|
+
if (btnMin) btnMin.title = panelMinimized ? 'Restaurar (Esc)' : 'Minimizar (Esc)';
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
window.__lrToggleMaximize = function() {
|
|
1265
|
+
if (panelMinimized) { window.__lrTogglePanel(); }
|
|
1266
|
+
panelMaximized = !panelMaximized;
|
|
1267
|
+
if (panelMaximized) {
|
|
1268
|
+
panel._prevTop = panel.style.top; panel._prevLeft = panel.style.left;
|
|
1269
|
+
panel._prevRight = panel.style.right; panel._prevWidth = panel.style.width;
|
|
1270
|
+
panel.style.top = '0'; panel.style.left = '0'; panel.style.right = '0';
|
|
1271
|
+
panel.style.width = '100vw'; panel.style.maxHeight = '100vh';
|
|
1272
|
+
panel.style.borderRadius = '0';
|
|
1273
|
+
} else {
|
|
1274
|
+
panel.style.top = panel._prevTop || '10px';
|
|
1275
|
+
panel.style.left = panel._prevLeft || 'auto';
|
|
1276
|
+
panel.style.right = panel._prevRight || '10px';
|
|
1277
|
+
panel.style.width = panel._prevWidth || PANEL_DEFAULT_WIDTH;
|
|
1278
|
+
panel.style.maxHeight = '90vh';
|
|
1279
|
+
panel.style.borderRadius = '10px';
|
|
1280
|
+
}
|
|
1281
|
+
// Actualizar icono del botón maximizar
|
|
1282
|
+
const btnMax = document.getElementById('lr-btn-maximize');
|
|
1283
|
+
if (btnMax) btnMax.innerHTML = panelMaximized ? '▫' : '□';
|
|
1284
|
+
if (btnMax) btnMax.title = panelMaximized ? 'Restaurar tamaño' : 'Maximizar';
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1288
|
+
// INIT
|
|
1289
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1290
|
+
renderPanel();
|
|
1291
|
+
const restoredCount = window.__capturedElements.length;
|
|
1292
|
+
|
|
1293
|
+
// Auto-reanudar grabación si estaba activa antes de la navegación
|
|
1294
|
+
let wasRecording = false;
|
|
1295
|
+
try { wasRecording = sessionStorage.getItem('__lr_recording') === '1'; } catch {}
|
|
1296
|
+
|
|
1297
|
+
if (wasRecording) {
|
|
1298
|
+
window.__lrStartRec(true);
|
|
1299
|
+
showStatus('Grabaci\u00f3n reanudada tras navegaci\u00f3n \u2014 ' + restoredCount + ' pasos previos');
|
|
1300
|
+
} else if (restoredCount > 0) {
|
|
1301
|
+
showStatus('Restauradas ' + restoredCount + ' capturas \u2014 "Limpiar" para empezar de cero');
|
|
1302
|
+
} else {
|
|
1303
|
+
showStatus('Grabador v4.0 activo \u2014 Grabar / Ctrl+Click / clic derecho = assert');
|
|
1304
|
+
}
|
|
1305
|
+
console.log('[LR] Grabador v4.0 inyectado. ' + restoredCount + ' capturas. Recording: ' + wasRecording);
|
|
1306
|
+
})();
|