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,986 @@
1
+ /**
2
+ * Grabador de Localizadores para Playwright v4.0
3
+ * Adaptado del Grabador Flotante Selenium de Juan Manuel Reina
4
+ *
5
+ * Uso:
6
+ * npx playwright test --config=playwright.recorder.config.ts
7
+ *
8
+ * Mejoras v4.0:
9
+ * - Assertions capturadas (clic derecho): toHaveText / toHaveValue / toBeVisible
10
+ * - Screenshots / visual baseline: boton Screenshot → toHaveScreenshot()
11
+ * - Modulos reutilizables: guardar/insertar secuencias JSON entre grabaciones
12
+ * Mejoras v3.0:
13
+ * - Modo Grabar automatico: captura clicks, fills y selects sin Ctrl+Click
14
+ * - Acciones reales en export TS: click(), fill('valor'), selectOption()
15
+ * - Boton flotante DETENER REC con animacion pulsante
16
+ * - Badges de accion con colores (naranja=click, azul=fill, morado=select)
17
+ * - Deduplicacion inteligente (400ms clicks, merge fills)
18
+ * Mejoras v2.0:
19
+ * - Descarga fiable via Node.js (page.exposeFunction) — no mas UUIDs
20
+ * - Agrupacion de localizadores por seccion/pagina
21
+ * - Export Playwright TS ejecutable con objetos agrupados + test scaffold
22
+ * - Persistencia de capturas entre navegaciones (sessionStorage)
23
+ * - Eliminar capturas individuales desde el panel
24
+ * - Overlay JS separado para mantenibilidad
25
+ */
26
+
27
+ import { test, expect, type Page } from '@playwright/test';
28
+ import { config as dotenvConfig } from 'dotenv';
29
+ import * as fs from 'fs';
30
+ import * as path from 'path';
31
+
32
+ dotenvConfig({ path: path.join(__dirname, '..', '.env') });
33
+
34
+ const CRED = {
35
+ user: process.env.TEST_USER || 'Carlos1',
36
+ password: process.env.TEST_PASS || '1234',
37
+ dbEngine: process.env.DB_ENGINE || 'sqlserver',
38
+ };
39
+ const OUTPUT_DIR = path.join(__dirname, '..', 'captured-locators');
40
+ const SCREENSHOTS_DIR = path.join(OUTPUT_DIR, 'screenshots'); // v4: visual baseline
41
+ const MODULES_DIR = path.join(OUTPUT_DIR, 'modules'); // v4: modulos reutilizables
42
+
43
+ // ─── Login reutilizado del flujo encuestas ──────────────────────
44
+ async function login(page: Page) {
45
+ await page.goto('/login');
46
+ await page.waitForLoadState('networkidle').catch(() => {});
47
+ await page.waitForTimeout(1500);
48
+
49
+ // Seleccionar BD desde .env (DB_ENGINE)
50
+ const dbCombo = page.locator('select').first();
51
+ if (await dbCombo.isVisible({ timeout: 3000 }).catch(() => false)) {
52
+ const options = await page.locator('select option').allTextContents();
53
+ const dbIndex = options.findIndex(o => new RegExp(CRED.dbEngine, 'i').test(o));
54
+ if (dbIndex >= 0) {
55
+ await dbCombo.selectOption({ index: dbIndex });
56
+ await page.waitForTimeout(500);
57
+ }
58
+ }
59
+
60
+ const userInput = page.locator(
61
+ '[data-automation-id="SDKCharField_BLLogin.Username"] input.dxbl-text-edit-input'
62
+ );
63
+ await userInput.waitFor({ state: 'visible', timeout: 30_000 });
64
+ // Esperar a que Blazor SDK termine de inicializar el campo
65
+ await page.locator('.sdk-loader').waitFor({ state: 'hidden', timeout: 10_000 }).catch(() => {});
66
+ await page.waitForTimeout(500);
67
+ await userInput.click();
68
+ await expect(userInput).toBeFocused({ timeout: 5_000 });
69
+ await userInput.fill(CRED.user);
70
+ await expect(userInput).toHaveValue(CRED.user, { timeout: 5_000 });
71
+ await page.locator('input[type="password"]').fill(CRED.password);
72
+ await page.locator('[data-automation-id="SDKButton_submit"]').click();
73
+ await page.waitForURL(/.*(?!.*login).*/, { timeout: 30_000 });
74
+ await page.waitForLoadState('networkidle').catch(() => {});
75
+ await page.waitForTimeout(3000);
76
+ }
77
+
78
+ // ─── Login Portal de Empleo (pre-load + pressSequentially) ──────
79
+ // Uso: PORTAL_LOGIN=1 PORTAL_URL='https://...' npx playwright test ...
80
+ async function loginPortal(page: Page) {
81
+ const portalUrl = process.env.PORTAL_URL!;
82
+ const portalUser = process.env.PORTAL_USER || 'user@example.com';
83
+ const portalPass = process.env.PORTAL_PASS || '';
84
+
85
+ // Pre-load: navegar al dominio base para que Blazor cargue recursos de localizacion
86
+ const baseUrl = portalUrl.replace(/\/Login\/?\?.*$/, '/');
87
+ await page.goto(baseUrl);
88
+ await page.waitForLoadState('networkidle').catch(() => {});
89
+ await page.waitForTimeout(3000);
90
+
91
+ // Ahora ir al login real con parametro dat=
92
+ await page.goto(portalUrl);
93
+ const userInput = page.locator(
94
+ '[data-automation-id="SDKCharField_BLLogin.Username"] input.dxbl-text-edit-input'
95
+ );
96
+ await userInput.waitFor({ state: 'visible', timeout: 30_000 });
97
+ await userInput.click();
98
+ await userInput.fill('');
99
+ await userInput.pressSequentially(portalUser, { delay: 30 });
100
+
101
+ const passInput = page.locator('[data-automation-id="SDKPasswordField_BLLogin.Password"]');
102
+ await passInput.click();
103
+ await passInput.fill(portalPass);
104
+
105
+ await page.locator('[data-automation-id="SDKButton_"][type="submit"]').click();
106
+ await page.waitForLoadState('networkidle').catch(() => {});
107
+ await page.waitForTimeout(3000);
108
+ console.log('[LR] Login portal completado');
109
+ }
110
+
111
+ // ─── Guardado server-side ───────────────────────────────────────
112
+ function saveLocatorFile(filename: string, content: string): string {
113
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
114
+ const filePath = path.join(OUTPUT_DIR, filename);
115
+ fs.writeFileSync(filePath, content, 'utf-8');
116
+ console.log(`[LR] Archivo guardado: ${filePath}`);
117
+ return filePath;
118
+ }
119
+
120
+ // ─── Generador de Playwright TS ejecutable ──────────────────────
121
+ function generatePlaywrightTS(captures: any[]): {
122
+ filename: string;
123
+ filePath: string;
124
+ content: string;
125
+ } {
126
+ const timestamp = new Date().toISOString();
127
+ const filename = `locators-${timestamp.slice(0, 19).replace(/:/g, '-')}.ts`;
128
+
129
+ if (captures.length === 0) {
130
+ const content = '// No hay elementos capturados\n';
131
+ const filePath = saveLocatorFile(filename, content);
132
+ return { filename, filePath, content };
133
+ }
134
+
135
+ // ── Agrupar capturas ──
136
+ const groups: Record<string, any[]> = {};
137
+ captures.forEach((c: any) => {
138
+ const g = c.group || 'General';
139
+ if (!groups[g]) groups[g] = [];
140
+ groups[g].push(c);
141
+ });
142
+
143
+ const urls = [...new Set(captures.map((c: any) => c.url).filter(Boolean))];
144
+ const groupNames = Object.keys(groups);
145
+
146
+ // ── Asignar nombres de variable unicos ──
147
+ const usedNames: Record<string, number> = {};
148
+ captures.forEach((c: any) => {
149
+ let base = '';
150
+ if (c.autoId) {
151
+ // Quitar prefijo SDK: SDKCharField_BLSurvey.Name → BLSurvey_Name
152
+ base = c.autoId
153
+ .replace(
154
+ /^SDK(CharField|NumericField|Button|DateField|ComboBox|CheckBox|Grid|DataGrid|Dialog|TreeView|RadioGroup|Toolbar|DropDown|ListBox|Tab|Panel|Switch|Menu|Popup|Upload)_/,
155
+ ''
156
+ )
157
+ .replace(/[^a-zA-Z0-9]/g, '_');
158
+ } else if (c.title) {
159
+ base = c.title.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30);
160
+ } else if (c.text) {
161
+ base = c.text
162
+ .replace(/[^a-zA-Z0-9\s]/g, '')
163
+ .trim()
164
+ .replace(/\s+/g, '_')
165
+ .substring(0, 20);
166
+ } else {
167
+ base = `${c.tag}_${c.num}`;
168
+ }
169
+ // Variable no puede empezar con numero
170
+ if (/^[0-9]/.test(base)) base = 'el_' + base;
171
+ // Evitar vacios
172
+ if (!base) base = `${c.tag}_${c.num}`;
173
+ // Deduplicar
174
+ if (usedNames[base]) {
175
+ usedNames[base]++;
176
+ base = `${base}_${usedNames[base]}`;
177
+ } else {
178
+ usedNames[base] = 1;
179
+ }
180
+ c._varName = base;
181
+ });
182
+
183
+ // ── Construir TS ──
184
+ const L: string[] = [];
185
+
186
+ L.push(`// Localizadores capturados \u2014 ${timestamp}`);
187
+ L.push('// Generado por Grabador de Localizadores Playwright v4.0');
188
+ if (urls.length > 0) L.push(`// URL(s): ${urls.join(', ')}`);
189
+ L.push(
190
+ `// Total: ${captures.length} elementos en ${groupNames.length} grupo(s)`
191
+ );
192
+ L.push('');
193
+ L.push("import { test, expect, type Page } from '@playwright/test';");
194
+ L.push('');
195
+
196
+ // v4: detectar modulos reutilizables insertados (source: 'module:<name>')
197
+ const moduleNames = [
198
+ ...new Set(
199
+ captures
200
+ .filter((c: any) => typeof c.source === 'string' && c.source.startsWith('module:'))
201
+ .map((c: any) => c.source.slice('module:'.length))
202
+ ),
203
+ ];
204
+ if (moduleNames.length > 0) {
205
+ L.push('// Modulos reutilizables detectados — descomenta para extraerlos a archivos compartidos:');
206
+ moduleNames.forEach((m: any) => {
207
+ const fn = String(m).replace(/[^a-zA-Z0-9]/g, '_');
208
+ L.push(`// import { ${fn} } from './modules/${m}';`);
209
+ });
210
+ L.push('');
211
+ }
212
+
213
+ // Detectar fills con sufijo numerico para auto-incremento
214
+ const fillsWithNumbers = captures.filter(
215
+ (c: any) => c.actionType === 'fill' && c.actionValue && /\d+\s*$/.test(c.actionValue)
216
+ );
217
+ const hasAutoIncrement = fillsWithNumbers.length > 0;
218
+ if (hasAutoIncrement) {
219
+ L.push('// Auto-incremento: si un registro ya existe, sube el numero');
220
+ L.push("const RUN_ID = Date.now().toString().slice(-4);");
221
+ L.push('');
222
+ }
223
+
224
+ // Objetos de localizadores por grupo
225
+ groupNames.forEach((groupName) => {
226
+ const safeName =
227
+ groupName.replace(/[^a-zA-Z0-9]/g, '') || 'General';
228
+ const items = groups[groupName];
229
+ const barLen = Math.max(1, 55 - groupName.length);
230
+ const bar = '\u2550'.repeat(barLen);
231
+
232
+ L.push(`// \u2550\u2550\u2550 ${groupName} ${bar}`);
233
+ L.push(`const ${safeName} = {`);
234
+
235
+ items.forEach((c: any) => {
236
+ // JSDoc comment con info del elemento
237
+ const parts = [`#${c.num}`, `<${c.tag}>`];
238
+ if (c.isDisabled) parts.push('[DISABLED]');
239
+ if (c.autoId) parts.push(c.autoId);
240
+ if (c.text && c.text !== c.autoId)
241
+ parts.push(`"${c.text.substring(0, 30)}"`);
242
+ L.push(` /** ${parts.join(' ')} */`);
243
+ L.push(` ${c._varName}: (page: Page) => ${c.bestPlaywright},`);
244
+
245
+ // Alternativas como comentarios
246
+ if (c.allStrategies && c.allStrategies.length > 1) {
247
+ c.allStrategies.slice(1, 3).forEach((s: any) => {
248
+ L.push(` // alt ${s.type} (${s.reliability}%): ${s.playwright}`);
249
+ });
250
+ }
251
+ });
252
+
253
+ L.push('};');
254
+ L.push('');
255
+ });
256
+
257
+ // Test scaffold
258
+ L.push(
259
+ '// \u2500\u2500\u2500 Test Scaffold \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
260
+ );
261
+ L.push("test.describe('Flujo capturado', () => {");
262
+ L.push(
263
+ " test('validar elementos capturados', async ({ page }) => {"
264
+ );
265
+
266
+ if (urls.length > 0) {
267
+ L.push(` await page.goto('${urls[0]}');`);
268
+ L.push('');
269
+ }
270
+
271
+ groupNames.forEach((groupName) => {
272
+ const safeName =
273
+ groupName.replace(/[^a-zA-Z0-9]/g, '') || 'General';
274
+ L.push(` // \u2500\u2500 ${groupName} \u2500\u2500`);
275
+ groups[groupName].forEach((c: any) => {
276
+ const expr = `${safeName}.${c._varName}(page)`;
277
+ if (c.isDisabled) {
278
+ L.push(
279
+ ` // await expect(${expr}).toBeVisible(); // DISABLED en captura`
280
+ );
281
+ } else if (c.actionType === 'click') {
282
+ L.push(` await ${expr}.click();`);
283
+ L.push(` await page.waitForLoadState('networkidle').catch(() => {});`);
284
+ L.push(` await page.waitForTimeout(1500);`);
285
+ } else if (c.actionType === 'fill') {
286
+ const raw = c.actionValue || 'valor';
287
+ const escaped = raw.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
288
+ // Auto-incremento: reemplazar sufijo numerico con RUN_ID
289
+ if (hasAutoIncrement && /\d+\s*$/.test(raw)) {
290
+ const prefix = escaped.replace(/\d+\s*$/, '').replace(/\s+$/, '');
291
+ L.push(` await ${expr}.fill('${prefix} ' + RUN_ID);`);
292
+ } else {
293
+ L.push(` await ${expr}.fill('${escaped}');`);
294
+ }
295
+ } else if (c.actionType === 'select') {
296
+ const val = c.actionValue
297
+ ? c.actionValue.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
298
+ : '';
299
+ L.push(` await ${expr}.selectOption('${val}');`);
300
+ } else if (c.actionType === 'assert') {
301
+ // v4: assertion capturada via clic derecho
302
+ const av = (c.assertionValue || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
303
+ if (c.assertionType === 'text') {
304
+ L.push(` await expect(${expr}).toHaveText('${av}');`);
305
+ } else if (c.assertionType === 'value') {
306
+ L.push(` await expect(${expr}).toHaveValue('${av}');`);
307
+ } else {
308
+ L.push(` await expect(${expr}).toBeVisible();`);
309
+ }
310
+ } else if (c.actionType === 'screenshot') {
311
+ // v4: visual baseline
312
+ const shot = c.screenshotFile || `step-${c.num}.png`;
313
+ L.push(` await expect(page).toHaveScreenshot('${shot}');`);
314
+ } else if (
315
+ c.tag === 'input' ||
316
+ c.tag === 'textarea' ||
317
+ c.tag === 'select'
318
+ ) {
319
+ // inspect mode — sugerir accion segun tag
320
+ L.push(` await expect(${expr}).toBeVisible();`);
321
+ L.push(` // await ${expr}.fill('valor');`);
322
+ } else if (
323
+ c.tag === 'button' ||
324
+ (c.autoId && c.autoId.includes('Button'))
325
+ ) {
326
+ L.push(` await expect(${expr}).toBeVisible();`);
327
+ L.push(` // await ${expr}.click();`);
328
+ } else {
329
+ L.push(` await expect(${expr}).toBeVisible();`);
330
+ }
331
+ });
332
+ L.push('');
333
+ });
334
+
335
+ L.push(' });');
336
+ L.push('});');
337
+ L.push('');
338
+
339
+ const content = L.join('\n');
340
+ const filePath = saveLocatorFile(filename, content);
341
+ return { filename, filePath, content };
342
+ }
343
+
344
+ // ─── Generador de Robot Framework (SeleniumLibrary) ─────────────
345
+ function generateRobotSelenium(captures: any[]): {
346
+ filename: string;
347
+ filePath: string;
348
+ content: string;
349
+ } {
350
+ const timestamp = new Date().toISOString();
351
+ const filename = `locators-${timestamp.slice(0, 19).replace(/:/g, '-')}.robot`;
352
+
353
+ if (captures.length === 0) {
354
+ const content = '# No hay elementos capturados\n';
355
+ const filePath = saveLocatorFile(filename, content);
356
+ return { filename, filePath, content };
357
+ }
358
+
359
+ const urls = [...new Set(captures.map((c: any) => c.url).filter(Boolean))];
360
+ const groups: Record<string, any[]> = {};
361
+ captures.forEach((c: any) => {
362
+ const g = c.group || 'General';
363
+ if (!groups[g]) groups[g] = [];
364
+ groups[g].push(c);
365
+ });
366
+ const groupNames = Object.keys(groups);
367
+
368
+ const L: string[] = [];
369
+ L.push('*** Settings ***');
370
+ L.push('Library SeleniumLibrary');
371
+ L.push('');
372
+ L.push(`# Localizadores capturados — ${timestamp}`);
373
+ L.push('# Generado por Grabador de Localizadores Playwright v4.0');
374
+ L.push(`# Total: ${captures.length} elementos en ${groupNames.length} grupo(s)`);
375
+ L.push('');
376
+ L.push('*** Test Cases ***');
377
+ L.push('Flujo Capturado');
378
+
379
+ if (urls.length > 0) {
380
+ L.push(` Open Browser ${urls[0]} chrome`);
381
+ L.push(' Maximize Browser Window');
382
+ L.push('');
383
+ }
384
+
385
+ groupNames.forEach((groupName) => {
386
+ L.push(` # ── ${groupName} ──`);
387
+ groups[groupName].forEach((c: any) => {
388
+ const selector = `css=${c.bestLocator}`;
389
+ const comment = ` # #${c.num} <${c.tag}>${c.autoId ? ' ' + c.autoId : ''}`;
390
+ L.push(comment);
391
+
392
+ if (c.actionType === 'click') {
393
+ L.push(` Wait Until Element Is Visible ${selector} timeout=10s`);
394
+ L.push(` Click Element ${selector}`);
395
+ L.push(' Sleep 1s');
396
+ } else if (c.actionType === 'fill') {
397
+ const val = c.actionValue || 'valor';
398
+ L.push(` Wait Until Element Is Visible ${selector} timeout=10s`);
399
+ L.push(` Input Text ${selector} ${val}`);
400
+ } else if (c.actionType === 'select') {
401
+ const val = c.actionValue || '';
402
+ L.push(` Wait Until Element Is Visible ${selector} timeout=10s`);
403
+ L.push(` Select From List By Value ${selector} ${val}`);
404
+ } else if (c.actionType === 'assert') {
405
+ // v4: assertion capturada
406
+ if (c.assertionType === 'text') {
407
+ L.push(` Element Text Should Be ${selector} ${c.assertionValue || ''}`);
408
+ } else if (c.assertionType === 'value') {
409
+ L.push(` Textfield Value Should Be ${selector} ${c.assertionValue || ''}`);
410
+ } else {
411
+ L.push(` Element Should Be Visible ${selector}`);
412
+ }
413
+ } else if (c.actionType === 'screenshot') {
414
+ // v4: visual baseline
415
+ L.push(` Capture Page Screenshot ${c.screenshotFile || 'step-' + c.num + '.png'}`);
416
+ } else {
417
+ L.push(` Wait Until Element Is Visible ${selector} timeout=10s`);
418
+ L.push(` # Element visible: ${selector}`);
419
+ }
420
+ });
421
+ L.push('');
422
+ });
423
+
424
+ if (urls.length > 0) {
425
+ L.push(' [Teardown] Close Browser');
426
+ }
427
+ L.push('');
428
+
429
+ const content = L.join('\n');
430
+ const filePath = saveLocatorFile(filename, content);
431
+ return { filename, filePath, content };
432
+ }
433
+
434
+ // ─── Generador de Playwright Python ─────────────────────────────
435
+ function generatePlaywrightPython(captures: any[]): {
436
+ filename: string;
437
+ filePath: string;
438
+ content: string;
439
+ } {
440
+ const timestamp = new Date().toISOString();
441
+ const filename = `locators-${timestamp.slice(0, 19).replace(/:/g, '-')}.py`;
442
+
443
+ if (captures.length === 0) {
444
+ const content = '# No hay elementos capturados\n';
445
+ const filePath = saveLocatorFile(filename, content);
446
+ return { filename, filePath, content };
447
+ }
448
+
449
+ const urls = [...new Set(captures.map((c: any) => c.url).filter(Boolean))];
450
+ const groups: Record<string, any[]> = {};
451
+ captures.forEach((c: any) => {
452
+ const g = c.group || 'General';
453
+ if (!groups[g]) groups[g] = [];
454
+ groups[g].push(c);
455
+ });
456
+ const groupNames = Object.keys(groups);
457
+
458
+ // Detectar fills con sufijo numerico para auto-incremento
459
+ const fillsWithNumbers = captures.filter(
460
+ (c: any) => c.actionType === 'fill' && c.actionValue && /\d+\s*$/.test(c.actionValue)
461
+ );
462
+ const hasAutoIncrement = fillsWithNumbers.length > 0;
463
+
464
+ const L: string[] = [];
465
+ L.push(`# Localizadores capturados — ${timestamp}`);
466
+ L.push('# Generado por Grabador de Localizadores Playwright v4.0');
467
+ L.push(`# Total: ${captures.length} elementos en ${groupNames.length} grupo(s)`);
468
+ L.push('');
469
+ L.push('import pytest');
470
+ L.push('from playwright.sync_api import Page, expect');
471
+ if (hasAutoIncrement) {
472
+ L.push('import time');
473
+ }
474
+ L.push('');
475
+ L.push('');
476
+
477
+ if (hasAutoIncrement) {
478
+ L.push('# Auto-incremento: si un registro ya existe, sube el numero');
479
+ L.push('RUN_ID = str(int(time.time()))[-4:]');
480
+ L.push('');
481
+ L.push('');
482
+ }
483
+
484
+ L.push('def test_flujo_capturado(page: Page):');
485
+ L.push(` """Flujo capturado desde el Grabador de Localizadores v4.0."""`);
486
+
487
+ if (urls.length > 0) {
488
+ L.push(` page.goto("${urls[0]}")`);
489
+ L.push('');
490
+ }
491
+
492
+ groupNames.forEach((groupName) => {
493
+ L.push(` # ── ${groupName} ──`);
494
+ groups[groupName].forEach((c: any) => {
495
+ const sel = c.bestLocator.replace(/"/g, '\\"');
496
+ const comment = ` # #${c.num} <${c.tag}>${c.autoId ? ' ' + c.autoId : ''}`;
497
+ L.push(comment);
498
+
499
+ if (c.actionType === 'click') {
500
+ L.push(` page.locator("${sel}").click()`);
501
+ L.push(' page.wait_for_load_state("networkidle")');
502
+ L.push(' page.wait_for_timeout(1500)');
503
+ } else if (c.actionType === 'fill') {
504
+ const raw = c.actionValue || 'valor';
505
+ const escaped = raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
506
+ if (hasAutoIncrement && /\d+\s*$/.test(raw)) {
507
+ const prefix = escaped.replace(/\d+\s*$/, '').replace(/\s+$/, '');
508
+ L.push(` page.locator("${sel}").fill(f"${prefix} {RUN_ID}")`);
509
+ } else {
510
+ L.push(` page.locator("${sel}").fill("${escaped}")`);
511
+ }
512
+ } else if (c.actionType === 'select') {
513
+ const val = (c.actionValue || '').replace(/"/g, '\\"');
514
+ L.push(` page.locator("${sel}").select_option("${val}")`);
515
+ } else if (c.actionType === 'assert') {
516
+ // v4: assertion capturada
517
+ const av = (c.assertionValue || '').replace(/"/g, '\\"');
518
+ if (c.assertionType === 'text') {
519
+ L.push(` expect(page.locator("${sel}")).to_have_text("${av}")`);
520
+ } else if (c.assertionType === 'value') {
521
+ L.push(` expect(page.locator("${sel}")).to_have_value("${av}")`);
522
+ } else {
523
+ L.push(` expect(page.locator("${sel}")).to_be_visible()`);
524
+ }
525
+ } else if (c.actionType === 'screenshot') {
526
+ // v4: visual baseline
527
+ L.push(` expect(page).to_have_screenshot("${c.screenshotFile || 'step-' + c.num + '.png'}")`);
528
+ } else {
529
+ L.push(` expect(page.locator("${sel}")).to_be_visible()`);
530
+ }
531
+ });
532
+ L.push('');
533
+ });
534
+ L.push('');
535
+
536
+ const content = L.join('\n');
537
+ const filePath = saveLocatorFile(filename, content);
538
+ return { filename, filePath, content };
539
+ }
540
+
541
+ // ─── Generador de Cypress ───────────────────────────────────────
542
+ function generateCypress(captures: any[]): {
543
+ filename: string;
544
+ filePath: string;
545
+ content: string;
546
+ } {
547
+ const timestamp = new Date().toISOString();
548
+ const filename = `locators-${timestamp.slice(0, 19).replace(/:/g, '-')}.cy.js`;
549
+
550
+ if (captures.length === 0) {
551
+ const content = '// No hay elementos capturados\n';
552
+ const filePath = saveLocatorFile(filename, content);
553
+ return { filename, filePath, content };
554
+ }
555
+
556
+ const urls = [...new Set(captures.map((c: any) => c.url).filter(Boolean))];
557
+ const groups: Record<string, any[]> = {};
558
+ captures.forEach((c: any) => {
559
+ const g = c.group || 'General';
560
+ if (!groups[g]) groups[g] = [];
561
+ groups[g].push(c);
562
+ });
563
+ const groupNames = Object.keys(groups);
564
+
565
+ // Detectar fills con sufijo numerico para auto-incremento
566
+ const fillsWithNumbers = captures.filter(
567
+ (c: any) => c.actionType === 'fill' && c.actionValue && /\d+\s*$/.test(c.actionValue)
568
+ );
569
+ const hasAutoIncrement = fillsWithNumbers.length > 0;
570
+
571
+ const L: string[] = [];
572
+ L.push(`// Localizadores capturados — ${timestamp}`);
573
+ L.push('// Generado por Grabador de Localizadores Playwright v4.0');
574
+ L.push(`// Total: ${captures.length} elementos en ${groupNames.length} grupo(s)`);
575
+ L.push('');
576
+
577
+ if (hasAutoIncrement) {
578
+ L.push('// Auto-incremento: si un registro ya existe, sube el numero');
579
+ L.push("const RUN_ID = Date.now().toString().slice(-4);");
580
+ L.push('');
581
+ }
582
+
583
+ L.push("describe('Flujo capturado', () => {");
584
+ L.push(" it('validar elementos capturados', () => {");
585
+
586
+ if (urls.length > 0) {
587
+ L.push(` cy.visit('${urls[0]}');`);
588
+ L.push('');
589
+ }
590
+
591
+ groupNames.forEach((groupName) => {
592
+ L.push(` // ── ${groupName} ──`);
593
+ groups[groupName].forEach((c: any) => {
594
+ const sel = c.bestLocator.replace(/'/g, "\\'");
595
+ const comment = ` // #${c.num} <${c.tag}>${c.autoId ? ' ' + c.autoId : ''}`;
596
+ L.push(comment);
597
+
598
+ if (c.actionType === 'click') {
599
+ L.push(` cy.get('${sel}').click();`);
600
+ L.push(' cy.wait(1500);');
601
+ } else if (c.actionType === 'fill') {
602
+ const raw = c.actionValue || 'valor';
603
+ const escaped = raw.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
604
+ if (hasAutoIncrement && /\d+\s*$/.test(raw)) {
605
+ const prefix = escaped.replace(/\d+\s*$/, '').replace(/\s+$/, '');
606
+ L.push(` cy.get('${sel}').clear().type('${prefix} ' + RUN_ID);`);
607
+ } else {
608
+ L.push(` cy.get('${sel}').clear().type('${escaped}');`);
609
+ }
610
+ } else if (c.actionType === 'select') {
611
+ const val = (c.actionValue || '').replace(/'/g, "\\'");
612
+ L.push(` cy.get('${sel}').select('${val}');`);
613
+ } else if (c.actionType === 'assert') {
614
+ // v4: assertion capturada
615
+ const av = (c.assertionValue || '').replace(/'/g, "\\'");
616
+ if (c.assertionType === 'text') {
617
+ L.push(` cy.get('${sel}').should('have.text', '${av}');`);
618
+ } else if (c.assertionType === 'value') {
619
+ L.push(` cy.get('${sel}').should('have.value', '${av}');`);
620
+ } else {
621
+ L.push(` cy.get('${sel}').should('exist');`);
622
+ }
623
+ } else if (c.actionType === 'screenshot') {
624
+ // v4: visual baseline
625
+ const shot = (c.screenshotFile || `step-${c.num}.png`).replace(/\.png$/, '');
626
+ L.push(` cy.screenshot('${shot}');`);
627
+ } else {
628
+ L.push(` cy.get('${sel}').should('be.visible');`);
629
+ }
630
+ });
631
+ L.push('');
632
+ });
633
+
634
+ L.push(' });');
635
+ L.push('});');
636
+ L.push('');
637
+
638
+ const content = L.join('\n');
639
+ const filePath = saveLocatorFile(filename, content);
640
+ return { filename, filePath, content };
641
+ }
642
+
643
+ // ─── Generador de CSV ───────────────────────────────────────────
644
+ function generateCSV(captures: any[]): {
645
+ filename: string;
646
+ filePath: string;
647
+ content: string;
648
+ } {
649
+ const timestamp = new Date().toISOString();
650
+ const filename = `locators-${timestamp.slice(0, 19).replace(/:/g, '-')}.csv`;
651
+
652
+ if (captures.length === 0) {
653
+ const content = '# No hay elementos capturados\n';
654
+ const filePath = saveLocatorFile(filename, content);
655
+ return { filename, filePath, content };
656
+ }
657
+
658
+ const headers = [
659
+ 'Paso', 'Grupo', 'Acción', 'Tag', 'AutomationId', 'Título',
660
+ 'Texto', 'Valor', 'TipoAssertion', 'ValorAssertion', 'Screenshot',
661
+ 'Selector', 'Playwright', 'Confiabilidad',
662
+ 'Habilitado', 'Visible', 'URL'
663
+ ];
664
+
665
+ const L: string[] = [];
666
+ L.push(headers.join(','));
667
+
668
+ captures.forEach((c: any) => {
669
+ const row = [
670
+ c.num,
671
+ csvEscape(c.group || 'General'),
672
+ csvEscape(c.actionType || 'inspect'),
673
+ csvEscape(c.tag),
674
+ csvEscape(c.autoId),
675
+ csvEscape(c.title),
676
+ csvEscape(c.text),
677
+ csvEscape(c.actionValue || ''),
678
+ csvEscape(c.assertionType || ''),
679
+ csvEscape(c.assertionValue || ''),
680
+ csvEscape(c.screenshotFile || ''),
681
+ csvEscape(c.bestLocator),
682
+ csvEscape(c.bestPlaywright),
683
+ c.reliability + '%',
684
+ c.isDisabled ? 'No' : 'Sí',
685
+ c.isVisible ? 'Sí' : 'No',
686
+ csvEscape(c.url || '')
687
+ ];
688
+ L.push(row.join(','));
689
+ });
690
+ L.push('');
691
+
692
+ const content = L.join('\n');
693
+ const filePath = saveLocatorFile(filename, content);
694
+ return { filename, filePath, content };
695
+ }
696
+
697
+ function csvEscape(val: string | null | undefined): string {
698
+ if (!val) return '""';
699
+ const s = String(val);
700
+ if (s.includes(',') || s.includes('"') || s.includes('\n')) {
701
+ return '"' + s.replace(/"/g, '""') + '"';
702
+ }
703
+ return s;
704
+ }
705
+
706
+ // ─── Generador de Gherkin (.feature) ────────────────────────────
707
+ function generateGherkin(captures: any[]): {
708
+ filename: string;
709
+ filePath: string;
710
+ content: string;
711
+ } {
712
+ const timestamp = new Date().toISOString();
713
+ const filename = `locators-${timestamp.slice(0, 19).replace(/:/g, '-')}.feature`;
714
+
715
+ if (captures.length === 0) {
716
+ const content = '# No hay elementos capturados\n';
717
+ const filePath = saveLocatorFile(filename, content);
718
+ return { filename, filePath, content };
719
+ }
720
+
721
+ const urls = [...new Set(captures.map((c: any) => c.url).filter(Boolean))];
722
+ const groups: Record<string, any[]> = {};
723
+ captures.forEach((c: any) => {
724
+ const g = c.group || 'General';
725
+ if (!groups[g]) groups[g] = [];
726
+ groups[g].push(c);
727
+ });
728
+ const groupNames = Object.keys(groups);
729
+
730
+ const L: string[] = [];
731
+ L.push(`# Generado por Grabador de Localizadores Playwright v4.0`);
732
+ L.push(`# ${timestamp}`);
733
+ L.push(`# Total: ${captures.length} pasos en ${groupNames.length} grupo(s)`);
734
+ L.push('');
735
+ L.push('Feature: Flujo capturado');
736
+ L.push(' Como usuario del ERP Siesa');
737
+ L.push(' Quiero ejecutar el flujo capturado');
738
+ L.push(' Para validar que los elementos funcionan correctamente');
739
+ L.push('');
740
+
741
+ L.push(' Scenario: Validar elementos capturados');
742
+
743
+ if (urls.length > 0) {
744
+ L.push(` Given I navigate to "${urls[0]}"`);
745
+ L.push('');
746
+ }
747
+
748
+ let stepIndex = 0;
749
+ groupNames.forEach((groupName) => {
750
+ L.push(` # ── ${groupName} ──`);
751
+ groups[groupName].forEach((c: any) => {
752
+ const sel = c.bestLocator;
753
+ const keyword = stepIndex === 0 ? 'When' : 'And';
754
+ const desc = c.autoId || c.title || c.text?.substring(0, 40) || c.tag;
755
+
756
+ if (c.actionType === 'click') {
757
+ L.push(` ${keyword} I click on "${desc}" # ${sel}`);
758
+ } else if (c.actionType === 'fill') {
759
+ const val = c.actionValue || 'valor';
760
+ L.push(` ${keyword} I fill "${desc}" with "${val}" # ${sel}`);
761
+ } else if (c.actionType === 'select') {
762
+ const val = c.actionValue || '';
763
+ L.push(` ${keyword} I select "${val}" from "${desc}" # ${sel}`);
764
+ } else if (c.actionType === 'assert') {
765
+ // v4: assertion capturada
766
+ if (c.assertionType === 'text') {
767
+ L.push(` Then el elemento "${desc}" debe mostrar "${c.assertionValue || ''}" # ${sel}`);
768
+ } else if (c.assertionType === 'value') {
769
+ L.push(` Then el elemento "${desc}" debe tener valor "${c.assertionValue || ''}" # ${sel}`);
770
+ } else {
771
+ L.push(` Then el elemento "${desc}" debe ser visible # ${sel}`);
772
+ }
773
+ } else if (c.actionType === 'screenshot') {
774
+ // v4: visual baseline
775
+ L.push(` And se captura screenshot de verificacion visual # ${c.screenshotFile || 'step-' + c.num + '.png'}`);
776
+ } else {
777
+ L.push(` ${keyword} I see "${desc}" # ${sel}`);
778
+ }
779
+ stepIndex++;
780
+ });
781
+ L.push('');
782
+ });
783
+
784
+ // Then final — verificar último elemento visible
785
+ const lastCapture = captures[captures.length - 1];
786
+ const lastDesc = lastCapture.autoId || lastCapture.title || lastCapture.text?.substring(0, 40) || lastCapture.tag;
787
+ L.push(` Then "${lastDesc}" should be visible # ${lastCapture.bestLocator}`);
788
+ L.push('');
789
+
790
+ const content = L.join('\n');
791
+ const filePath = saveLocatorFile(filename, content);
792
+ return { filename, filePath, content };
793
+ }
794
+
795
+ // ─── Overlay JS (cargado desde archivo separado) ────────────────
796
+ const OVERLAY_JS = fs.readFileSync(
797
+ path.join(__dirname, 'locator-overlay.js'),
798
+ 'utf-8'
799
+ );
800
+
801
+ // ─── Test principal ─────────────────────────────────────────────
802
+ test.describe('Grabador de Localizadores', () => {
803
+ let page: Page;
804
+
805
+ test.beforeAll(async ({ browser }) => {
806
+ page = await browser.newPage();
807
+
808
+ // Exponer funciones Node.js al navegador para guardado fiable
809
+ await page.exposeFunction(
810
+ '__lrSaveJSON',
811
+ (json: string) => {
812
+ const timestamp = new Date().toISOString();
813
+ const filename = `locators-${timestamp
814
+ .slice(0, 19)
815
+ .replace(/:/g, '-')}.json`;
816
+ const formatted = JSON.stringify(JSON.parse(json), null, 2);
817
+ const filePath = saveLocatorFile(filename, formatted);
818
+ return { filename, filePath };
819
+ }
820
+ );
821
+
822
+ // Helper: wraps generator to catch errors and return a safe result
823
+ function safeGenerate<T extends { filename: string; filePath: string; content: string }>(
824
+ name: string,
825
+ ext: string,
826
+ fn: (captures: any[]) => T,
827
+ json: string
828
+ ): T {
829
+ try {
830
+ return fn(JSON.parse(json));
831
+ } catch (e: any) {
832
+ console.error(`[LR] Error in ${name}:`, e.message, e.stack);
833
+ return { filename: `error${ext}`, filePath: '', content: `// Error generando ${name}: ${e.message}` } as T;
834
+ }
835
+ }
836
+
837
+ await page.exposeFunction(
838
+ '__lrGenerateTS',
839
+ (json: string) => safeGenerate('generatePlaywrightTS', '.ts', generatePlaywrightTS, json)
840
+ );
841
+
842
+ await page.exposeFunction(
843
+ '__lrGenerateRobot',
844
+ (json: string) => safeGenerate('generateRobotSelenium', '.robot', generateRobotSelenium, json)
845
+ );
846
+
847
+ await page.exposeFunction(
848
+ '__lrGeneratePwPy',
849
+ (json: string) => safeGenerate('generatePlaywrightPython', '.py', generatePlaywrightPython, json)
850
+ );
851
+
852
+ await page.exposeFunction(
853
+ '__lrGenerateCypress',
854
+ (json: string) => safeGenerate('generateCypress', '.cy.js', generateCypress, json)
855
+ );
856
+
857
+ await page.exposeFunction(
858
+ '__lrGenerateGherkin',
859
+ (json: string) => safeGenerate('generateGherkin', '.feature', generateGherkin, json)
860
+ );
861
+
862
+ await page.exposeFunction(
863
+ '__lrGenerateCSV',
864
+ (json: string) => safeGenerate('generateCSV', '.csv', generateCSV, json)
865
+ );
866
+
867
+ // ── v4: Mejora 2 — capturar screenshot de pagina completa ──
868
+ await page.exposeFunction('__lrCaptureScreenshot', async (stepNum: number) => {
869
+ fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
870
+ const filename = `step-${stepNum}.png`;
871
+ const filePath = path.join(SCREENSHOTS_DIR, filename);
872
+ await page.screenshot({ path: filePath, fullPage: true });
873
+ console.log(`[LR] Screenshot guardado: ${filePath}`);
874
+ return { filename, filePath };
875
+ });
876
+
877
+ // ── v4: Mejora 3 — modulos reutilizables ──
878
+ await page.exposeFunction('__lrSaveModule', (json: string) => {
879
+ fs.mkdirSync(MODULES_DIR, { recursive: true });
880
+ const mod = JSON.parse(json);
881
+ const safeName = String(mod.name || 'modulo').replace(/[^a-zA-Z0-9_-]/g, '-');
882
+ const filename = `${safeName}.json`;
883
+ const filePath = path.join(MODULES_DIR, filename);
884
+ fs.writeFileSync(filePath, JSON.stringify(mod, null, 2), 'utf-8');
885
+ console.log(`[LR] Modulo guardado: ${filePath}`);
886
+ return { filename, filePath };
887
+ });
888
+
889
+ await page.exposeFunction('__lrListModules', () => {
890
+ if (!fs.existsSync(MODULES_DIR)) return JSON.stringify([]);
891
+ const modules = fs
892
+ .readdirSync(MODULES_DIR)
893
+ .filter((f) => f.endsWith('.json'))
894
+ .map((f) => {
895
+ try {
896
+ const mod = JSON.parse(fs.readFileSync(path.join(MODULES_DIR, f), 'utf-8'));
897
+ return {
898
+ name: mod.name || f.replace(/\.json$/, ''),
899
+ displayName: mod.displayName || mod.name || f.replace(/\.json$/, ''),
900
+ description: mod.description || '',
901
+ stepCount: mod.stepCount || (mod.captures ? mod.captures.length : 0),
902
+ };
903
+ } catch {
904
+ return null;
905
+ }
906
+ })
907
+ .filter(Boolean);
908
+ return JSON.stringify(modules);
909
+ });
910
+
911
+ await page.exposeFunction('__lrLoadModule', (name: string) => {
912
+ const safeName = String(name).replace(/[^a-zA-Z0-9_-]/g, '-');
913
+ const filePath = path.join(MODULES_DIR, `${safeName}.json`);
914
+ if (!fs.existsSync(filePath)) throw new Error(`Modulo no encontrado: ${safeName}`);
915
+ return fs.readFileSync(filePath, 'utf-8');
916
+ });
917
+
918
+ // PORTAL_LOGIN=1 para Portal de Empleo (pre-load + login automatico)
919
+ // SKIP_LOGIN=1 para portales públicos (no requieren login ERP)
920
+ if (process.env.PORTAL_LOGIN === '1' && process.env.PORTAL_URL) {
921
+ await loginPortal(page);
922
+ } else if ((process.env.SKIP_LOGIN || '').trim() !== '1') {
923
+ await login(page);
924
+ }
925
+ });
926
+
927
+ test.afterAll(async () => {
928
+ // No cerrar — el usuario navega manualmente
929
+ });
930
+
931
+ test('Iniciar grabador interactivo', async () => {
932
+ // Navegar a URL inicial si se especificó via env var
933
+ const startUrl = process.env.START_URL;
934
+ const skipLogin = (process.env.SKIP_LOGIN || '').trim() === '1';
935
+ if (startUrl) {
936
+ await page.goto(startUrl);
937
+ await page.waitForLoadState('domcontentloaded');
938
+ await page.waitForTimeout(2000);
939
+ } else if (skipLogin) {
940
+ await page.goto('https://www.google.com');
941
+ await page.waitForLoadState('domcontentloaded');
942
+ }
943
+
944
+ // Inyectar overlay
945
+ await page.evaluate(OVERLAY_JS);
946
+
947
+ console.log(
948
+ '\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
949
+ );
950
+ console.log(' GRABADOR DE LOCALIZADORES v4.0 \u2014 ACTIVO');
951
+ console.log(
952
+ '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
953
+ );
954
+ console.log(' Grabar \u2192 captura automatica de clicks/fills/selects');
955
+ console.log(' Ctrl+Click \u2192 captura manual (modo inspeccion)');
956
+ console.log(' Shift+Click \u2192 capturar elemento + hijos');
957
+ console.log(' Clic derecho \u2192 assertion (texto/existe/valor) [solo grabando]');
958
+ console.log(' Screenshot \u2192 captura visual baseline (toHaveScreenshot)');
959
+ console.log(' Modulos \u2192 guardar/insertar secuencias reutilizables');
960
+ console.log(' Hover \u2192 ver localizadores del elemento');
961
+ console.log(' Escape \u2192 minimizar/restaurar panel');
962
+ console.log(' Grupo \u2192 agrupa capturas para el export TS');
963
+ console.log(' Panel \u2192 Detectar / Exportar / Copiar');
964
+ console.log(
965
+ '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
966
+ );
967
+ console.log(` Archivos se guardan en: ${OUTPUT_DIR}`);
968
+ console.log(
969
+ '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n'
970
+ );
971
+
972
+ // Re-inyectar overlay despues de cada navegacion
973
+ page.on('load', async () => {
974
+ try {
975
+ await page.waitForTimeout(1000);
976
+ await page.evaluate(OVERLAY_JS);
977
+ console.log('[LR] Overlay re-inyectado tras navegacion');
978
+ } catch {
979
+ /* pagina cerrada */
980
+ }
981
+ });
982
+
983
+ // Mantener el navegador abierto 1 hora
984
+ await page.waitForTimeout(3_600_000);
985
+ });
986
+ });