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.
Files changed (49) hide show
  1. package/bmad/bmm/config.yaml +33 -0
  2. package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
  3. package/claude/skills/sa-aplicar/SKILL.md +268 -0
  4. package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
  5. package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
  6. package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
  7. package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
  8. package/claude/skills/sa-onboard-db/SKILL.md +122 -0
  9. package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
  10. package/claude/skills/sa-quality-process/SKILL.md +6 -0
  11. package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
  12. package/package.json +1 -1
  13. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
  14. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
  15. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
  16. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
  17. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
  18. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
  19. package/siesa-agents/resources/playwright-kit/.env.example +42 -0
  20. package/siesa-agents/resources/playwright-kit/README.md +228 -0
  21. package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
  22. package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
  23. package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
  24. package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
  25. package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
  26. package/siesa-agents/resources/playwright-kit/package.json +27 -0
  27. package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
  28. package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
  29. package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
  30. package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
  31. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
  32. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
  33. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
  34. package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
  35. package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
  36. package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
  37. package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
  38. package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
  39. package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
  40. package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
  41. package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
  42. package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
  43. package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
  44. package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
  45. package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
  46. package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
  47. package/siesa-agents/scripts/merge_test_design.py +106 -106
  48. package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
  49. 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)">&#8722;</button>
326
+ <button class="lr-window-btn" id="lr-btn-maximize" title="Maximizar / Restaurar">&#9633;</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">&#9210; Grabar</button>
331
+ <button class="lr-btn-rec lr-btn-rec-stop" id="lr-btn-stop" disabled>&#9209; 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">&lt;' + info.tag + '&gt;</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">&lt;' + cap.tag + '&gt;';
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 ? '&#43;' : '&#8722;';
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 ? '&#9643;' : '&#9633;';
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
+ })();