siesa-agents 2.1.72-qa.2 → 2.1.72-qa.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bmad/bmm/config.yaml +33 -0
- package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
- package/claude/skills/sa-aplicar/SKILL.md +268 -0
- package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
- package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
- package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
- package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
- package/claude/skills/sa-onboard-db/SKILL.md +122 -0
- package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
- package/claude/skills/sa-quality-process/SKILL.md +6 -0
- package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
- package/package.json +1 -1
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
- package/siesa-agents/resources/playwright-kit/.env.example +42 -0
- package/siesa-agents/resources/playwright-kit/README.md +228 -0
- package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
- package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
- package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
- package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
- package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
- package/siesa-agents/resources/playwright-kit/package.json +27 -0
- package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
- package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
- package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
- package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
- package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
- package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
- package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
- package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
- package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
- package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
- package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
- package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
- package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
- package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
- package/siesa-agents/scripts/merge_test_design.py +106 -106
- package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
- package/siesa-agents/scripts/requirements.txt +7 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blazor E2E Helpers — Módulo reutilizable para specs GP HCM
|
|
3
|
+
* Siesa Business ERP (Blazor Server + DevExpress + Radzen + SignalR)
|
|
4
|
+
*
|
|
5
|
+
* Extraído de Replica 4 (Capacitaciones) con refinamientos arquitectónicos.
|
|
6
|
+
* Patrones validados en R1-R4 (4 módulos, 191+ tests).
|
|
7
|
+
*
|
|
8
|
+
* Uso:
|
|
9
|
+
* import { createObsCollector, login, searchInGrid, ... } from './blazor-e2e-helpers';
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { expect, type Page } from '@playwright/test';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
// ─── Observability Collector ─────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface ObsEntry {
|
|
19
|
+
timestamp: string;
|
|
20
|
+
type: 'console-error' | 'console-warn' | 'js-exception' | 'http-error' | 'app-alert';
|
|
21
|
+
message: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createObsCollector() {
|
|
26
|
+
const entries: ObsEntry[] = [];
|
|
27
|
+
const now = () => new Date().toISOString().slice(11, 23);
|
|
28
|
+
|
|
29
|
+
function attach(page: Page) {
|
|
30
|
+
page.on('console', msg => {
|
|
31
|
+
if (msg.type() === 'error') {
|
|
32
|
+
entries.push({ timestamp: now(), type: 'console-error', message: msg.text().slice(0, 300) });
|
|
33
|
+
} else if (msg.type() === 'warning') {
|
|
34
|
+
entries.push({ timestamp: now(), type: 'console-warn', message: msg.text().slice(0, 300) });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
page.on('pageerror', err => {
|
|
39
|
+
entries.push({ timestamp: now(), type: 'js-exception', message: String(err).slice(0, 300) });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
page.on('response', resp => {
|
|
43
|
+
if (resp.status() >= 400) {
|
|
44
|
+
entries.push({
|
|
45
|
+
timestamp: now(),
|
|
46
|
+
type: 'http-error',
|
|
47
|
+
message: `${resp.status()} ${resp.statusText()}`,
|
|
48
|
+
url: resp.url().slice(0, 200),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Auto-accept dialogs (confirm/alert/prompt) para demo autónoma
|
|
54
|
+
page.on('dialog', async dialog => {
|
|
55
|
+
entries.push({
|
|
56
|
+
timestamp: now(),
|
|
57
|
+
type: 'app-alert',
|
|
58
|
+
message: `[${dialog.type()}] ${dialog.message().slice(0, 300)}`,
|
|
59
|
+
});
|
|
60
|
+
await dialog.accept().catch(() => {});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function report(suiteName: string) {
|
|
65
|
+
console.log(`\n╔══ OBSERVABILITY REPORT — ${suiteName} ══╗`);
|
|
66
|
+
console.log(`║ Total entries: ${entries.length}`);
|
|
67
|
+
const byType = entries.reduce((acc, e) => { acc[e.type] = (acc[e.type] || 0) + 1; return acc; }, {} as Record<string, number>);
|
|
68
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
69
|
+
console.log(`║ ${type}: ${count}`);
|
|
70
|
+
}
|
|
71
|
+
if (entries.length > 0) {
|
|
72
|
+
console.log('║ ─── Detalle (max 20) ───');
|
|
73
|
+
for (const e of entries.slice(0, 20)) {
|
|
74
|
+
console.log(`║ [${e.timestamp}] ${e.type}: ${e.message}${e.url ? ` (${e.url})` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
if (entries.length > 20) console.log(`║ ... y ${entries.length - 20} mas`);
|
|
77
|
+
}
|
|
78
|
+
console.log(`╚${'═'.repeat(40)}╝\n`);
|
|
79
|
+
|
|
80
|
+
// JSON file output
|
|
81
|
+
const reportData = {
|
|
82
|
+
suite: suiteName,
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
total: entries.length,
|
|
85
|
+
byType,
|
|
86
|
+
entries,
|
|
87
|
+
};
|
|
88
|
+
const outDir = path.join(process.cwd(), 'test-results');
|
|
89
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
90
|
+
const jsonPath = path.join(outDir, 'observability-report.json');
|
|
91
|
+
fs.writeFileSync(jsonPath, JSON.stringify(reportData, null, 2));
|
|
92
|
+
console.log(`Observability JSON → ${jsonPath}`);
|
|
93
|
+
|
|
94
|
+
return reportData;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { entries, attach, report };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Settle (espera condicional Blazor) ──────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Espera condicional a que Blazor/SignalR termine de renderizar:
|
|
104
|
+
* red en reposo + loader SDK oculto + un pequeño margen configurable.
|
|
105
|
+
* Reemplaza los `waitForTimeout()` incondicionales largos (regla R-CI-01).
|
|
106
|
+
* El margen se ajusta con la env var BLAZOR_SETTLE_MS (default 1000 ms);
|
|
107
|
+
* en CI lento se puede subir sin tocar el código.
|
|
108
|
+
*/
|
|
109
|
+
const SETTLE_MS = Number(process.env.BLAZOR_SETTLE_MS ?? 1000);
|
|
110
|
+
|
|
111
|
+
export async function settle(page: Page, extraMs = 0) {
|
|
112
|
+
await page.waitForLoadState('networkidle').catch(() => {});
|
|
113
|
+
await page.locator('.sdk-loader').waitFor({ state: 'hidden', timeout: 15_000 }).catch(() => {});
|
|
114
|
+
const margin = SETTLE_MS + extraMs;
|
|
115
|
+
if (margin > 0) await page.waitForTimeout(margin);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Login ───────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Selecciona la BD en el login si el combo está presente y `DB_ENGINE` está definido.
|
|
122
|
+
* Opt-in y best-effort: si no hay combo, `DB_ENGINE` no está, o algo falla, NO lanza
|
|
123
|
+
* y deja el login directo (comportamiento validado R1-R9 intacto).
|
|
124
|
+
* Soporta los dos mecanismos del ERP: DevExpress (SDKDropDownField) y <select> nativo.
|
|
125
|
+
*/
|
|
126
|
+
export async function selectDbIfPresent(page: Page, db?: string) {
|
|
127
|
+
const bd = db ?? process.env.DB_ENGINE;
|
|
128
|
+
if (!bd) return;
|
|
129
|
+
try {
|
|
130
|
+
// 1) DevExpress dropdown (SDKDropDownField_BLLogin.ConnectionName)
|
|
131
|
+
const ddField = page.locator('[data-automation-id="SDKDropDownField_BLLogin.ConnectionName"]');
|
|
132
|
+
if (await ddField.isVisible({ timeout: 2_000 }).catch(() => false)) {
|
|
133
|
+
await ddField.click();
|
|
134
|
+
const opt = page.getByText(new RegExp(bd, 'i')).first();
|
|
135
|
+
if (await opt.isVisible({ timeout: 2_000 }).catch(() => false)) {
|
|
136
|
+
await opt.click();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 2) <select> nativo
|
|
141
|
+
const sel = page.locator('select').first();
|
|
142
|
+
if (await sel.isVisible({ timeout: 2_000 }).catch(() => false)) {
|
|
143
|
+
const options = await sel.locator('option').allTextContents();
|
|
144
|
+
const idx = options.findIndex(o => new RegExp(bd, 'i').test(o));
|
|
145
|
+
if (idx >= 0) await sel.selectOption({ index: idx });
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// best-effort: ante cualquier fallo, continuar con login directo
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function login(page: Page, user?: string, pass?: string, db?: string) {
|
|
153
|
+
const cred = {
|
|
154
|
+
user: user || process.env.TEST_USER || 'Carlos1',
|
|
155
|
+
password: pass || process.env.TEST_PASS || '1234',
|
|
156
|
+
};
|
|
157
|
+
if (!user && !process.env.TEST_USER) {
|
|
158
|
+
console.warn('[blazor login] TEST_USER no definido — usando credencial demo "Carlos1". Define TEST_USER/TEST_PASS en .env para tu entorno.');
|
|
159
|
+
}
|
|
160
|
+
await page.goto('/login');
|
|
161
|
+
// Opt-in: solo selecciona BD si DB_ENGINE está definido y el combo existe (si no, login directo).
|
|
162
|
+
await selectDbIfPresent(page, db);
|
|
163
|
+
const userInput = page.locator(
|
|
164
|
+
'[data-automation-id="SDKCharField_BLLogin.Username"] input.dxbl-text-edit-input'
|
|
165
|
+
);
|
|
166
|
+
await userInput.waitFor({ state: 'visible', timeout: 30_000 });
|
|
167
|
+
await userInput.click();
|
|
168
|
+
await userInput.fill('');
|
|
169
|
+
await userInput.pressSequentially(cred.user, { delay: 50 });
|
|
170
|
+
|
|
171
|
+
await page.locator('[data-automation-id="SDKPasswordField_"]').click();
|
|
172
|
+
await page.locator('[data-automation-id="SDKPasswordField_"]').fill(cred.password);
|
|
173
|
+
|
|
174
|
+
await page.locator('[data-automation-id="SDKButton_submit"]').click();
|
|
175
|
+
try {
|
|
176
|
+
await page.waitForURL(/.*(?!.*login).*/, { timeout: 30_000 });
|
|
177
|
+
} catch (e) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`login: no se completó la autenticación para el usuario "${cred.user}". ` +
|
|
180
|
+
`Verifica TEST_USER/TEST_PASS y BASE_URL. Causa: ${e instanceof Error ? e.message : String(e)}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
await settle(page);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Form Helpers ────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export async function fillSDKText(page: Page, automationId: string, value: string) {
|
|
189
|
+
const input = page.locator(`[data-automation-id="${automationId}"] input.dxbl-text-edit-input`);
|
|
190
|
+
await input.click({ force: true });
|
|
191
|
+
await input.fill('');
|
|
192
|
+
await input.pressSequentially(value, { delay: 30 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function fillSDKTextArea(page: Page, automationId: string, value: string) {
|
|
196
|
+
const textarea = page.locator(`[data-automation-id="${automationId}"] textarea`);
|
|
197
|
+
await textarea.click({ force: true });
|
|
198
|
+
await textarea.fill('');
|
|
199
|
+
await textarea.pressSequentially(value, { delay: 30 });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function fillFechaRadzen(page: Page, automationId: string, fechaMMDDYYYY: string) {
|
|
203
|
+
const dateInput = page.locator(`[data-automation-id="${automationId}"] input.rz-inputtext`);
|
|
204
|
+
await dateInput.click();
|
|
205
|
+
await dateInput.fill('');
|
|
206
|
+
await dateInput.pressSequentially(fechaMMDDYYYY, { delay: 30 });
|
|
207
|
+
await page.keyboard.press('Enter');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Toolbar & UI ────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/** Click toolbar button (Create, Edit, Save, etc.) — handles duplicate buttons from hidden sidebar */
|
|
213
|
+
export async function clickToolbar(page: Page, action: string) {
|
|
214
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
215
|
+
await page.waitForTimeout(500);
|
|
216
|
+
const allBtns = page.locator(`[data-automation-id="TopBarButton_Action.${action}"]`);
|
|
217
|
+
const count = await allBtns.count();
|
|
218
|
+
const btn = count > 1 ? allBtns.last() : allBtns.first();
|
|
219
|
+
try {
|
|
220
|
+
await btn.click({ force: true, timeout: 5_000 });
|
|
221
|
+
} catch {
|
|
222
|
+
await btn.evaluate(el => (el as HTMLElement).click());
|
|
223
|
+
}
|
|
224
|
+
await settle(page);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function closeOverlays(page: Page) {
|
|
228
|
+
const dialogClose = page.locator('.rz-dialog-titlebar-close');
|
|
229
|
+
if (await dialogClose.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
230
|
+
await dialogClose.click();
|
|
231
|
+
}
|
|
232
|
+
const notifClose = page.locator('.rz-notification-close');
|
|
233
|
+
if (await notifClose.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
234
|
+
await notifClose.click();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function clickActivo(page: Page) {
|
|
239
|
+
const activoBtns = page.locator('[data-automation-id="SDKSelectBar_Status"]')
|
|
240
|
+
.getByText('Activo', { exact: true });
|
|
241
|
+
const activoBtn = (await activoBtns.count()) > 1 ? activoBtns.last() : activoBtns.first();
|
|
242
|
+
await activoBtn.click();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function waitForSave(page: Page) {
|
|
246
|
+
await page.locator('.sdk-loader').waitFor({ state: 'hidden', timeout: 15_000 }).catch(() => {});
|
|
247
|
+
await page.waitForTimeout(1500);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Entity Popup ────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Seleccionar entidad via popup lupa.
|
|
254
|
+
* Dual-mode: formulario búsqueda (Código+Buscar) vs grid directo (Celda de filtro+Enter).
|
|
255
|
+
* Maneja overlay search_back_fade con force:true.
|
|
256
|
+
*/
|
|
257
|
+
export async function selectEntityPopup(page: Page, entityAutomationId: string, searchText?: string) {
|
|
258
|
+
// Click lupa
|
|
259
|
+
const lupaIcon = page.locator(`[data-automation-id="${entityAutomationId}_SearchBtn"] i.fa-solid.fa-magnifying-glass`);
|
|
260
|
+
if (await lupaIcon.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
261
|
+
await lupaIcon.click();
|
|
262
|
+
} else {
|
|
263
|
+
const lupaBtn = page.locator(`[data-automation-id="${entityAutomationId}_SearchBtn"]`);
|
|
264
|
+
if (await lupaBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
265
|
+
await lupaBtn.click();
|
|
266
|
+
} else {
|
|
267
|
+
await page.evaluate(() => {
|
|
268
|
+
const icons = document.querySelectorAll('i.fa-solid.fa-magnifying-glass');
|
|
269
|
+
if (icons.length > 0) (icons[0] as HTMLElement).click();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
await settle(page);
|
|
274
|
+
|
|
275
|
+
// Detectar modo popup
|
|
276
|
+
const buscarBtn = page.getByRole('button', { name: 'Buscar' });
|
|
277
|
+
const hasBuscarForm = await buscarBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
|
278
|
+
|
|
279
|
+
if (hasBuscarForm) {
|
|
280
|
+
if (searchText) {
|
|
281
|
+
const dialogInputs = page.locator('.rz-dialog-wrapper input[type="text"], .rz-dialog-wrapper input.dxbl-text-edit-input');
|
|
282
|
+
const firstInput = dialogInputs.first();
|
|
283
|
+
if (await firstInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
284
|
+
await firstInput.click({ force: true });
|
|
285
|
+
await firstInput.fill('');
|
|
286
|
+
await firstInput.pressSequentially(searchText, { delay: 30 });
|
|
287
|
+
await page.waitForTimeout(500);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await buscarBtn.click();
|
|
291
|
+
await settle(page);
|
|
292
|
+
} else if (searchText) {
|
|
293
|
+
const filterInput = page.getByRole('textbox', { name: 'Celda de filtro' }).first();
|
|
294
|
+
if (await filterInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
295
|
+
await filterInput.click();
|
|
296
|
+
await filterInput.fill(searchText);
|
|
297
|
+
await page.keyboard.press('Enter');
|
|
298
|
+
await settle(page);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Seleccionar primera fila (force para overlay)
|
|
303
|
+
const row = page.locator('tr.rz-data-row, tr.dx-data-row').first();
|
|
304
|
+
if (await row.isVisible({ timeout: 10_000 }).catch(() => false)) {
|
|
305
|
+
try { await row.click({ force: true }); }
|
|
306
|
+
catch { await row.evaluate(el => (el as HTMLElement).click()); }
|
|
307
|
+
await page.waitForTimeout(1000);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await closeOverlays(page);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Grid Search (Listados) ─────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Buscar y seleccionar un registro en un listado Blazor.
|
|
317
|
+
* Dual-mode: formulario de búsqueda (Buscar btn) vs grid con "Celda de filtro".
|
|
318
|
+
* Presiona Enter después de llenar filtro para activar binding DevExpress.
|
|
319
|
+
*/
|
|
320
|
+
export async function searchInGrid(
|
|
321
|
+
page: Page,
|
|
322
|
+
listUrl: string,
|
|
323
|
+
searchCode: string,
|
|
324
|
+
opts?: { clickEdit?: boolean; filterColumn?: number }
|
|
325
|
+
) {
|
|
326
|
+
const filterCol = opts?.filterColumn ?? 0;
|
|
327
|
+
const clickEdit = opts?.clickEdit ?? true;
|
|
328
|
+
|
|
329
|
+
// 1. Navegar al listado
|
|
330
|
+
await page.goto(listUrl);
|
|
331
|
+
await settle(page);
|
|
332
|
+
|
|
333
|
+
// 2. Detectar modo
|
|
334
|
+
const buscarBtn = page.getByRole('button', { name: 'Buscar' });
|
|
335
|
+
const hasBuscarForm = await buscarBtn.isVisible({ timeout: 3000 }).catch(() => false);
|
|
336
|
+
|
|
337
|
+
if (hasBuscarForm) {
|
|
338
|
+
const dialogInputs = page.locator('input[type="text"], input.dxbl-text-edit-input');
|
|
339
|
+
const firstInput = dialogInputs.first();
|
|
340
|
+
if (await firstInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
341
|
+
await firstInput.click({ force: true });
|
|
342
|
+
await firstInput.fill(searchCode);
|
|
343
|
+
}
|
|
344
|
+
await buscarBtn.click();
|
|
345
|
+
await settle(page);
|
|
346
|
+
} else {
|
|
347
|
+
const filterCells = page.getByRole('textbox', { name: 'Celda de filtro' });
|
|
348
|
+
if (await filterCells.first().isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
349
|
+
const target = filterCells.nth(filterCol);
|
|
350
|
+
await target.click();
|
|
351
|
+
await target.fill(searchCode);
|
|
352
|
+
await page.keyboard.press('Enter');
|
|
353
|
+
await settle(page);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 3. Seleccionar fila
|
|
358
|
+
const row = page.locator('tr.rz-data-row, tr.dx-data-row').first();
|
|
359
|
+
const rowVisible = await row.isVisible({ timeout: 10_000 }).catch(() => false);
|
|
360
|
+
|
|
361
|
+
if (rowVisible && clickEdit) {
|
|
362
|
+
const editIcon = row.locator('i.fa-pencil, i.fa-pen, i.fa-pen-to-square, i.fa-edit').first();
|
|
363
|
+
const hasEditIcon = await editIcon.isVisible({ timeout: 2000 }).catch(() => false);
|
|
364
|
+
if (hasEditIcon) {
|
|
365
|
+
try { await editIcon.click({ force: true }); }
|
|
366
|
+
catch { await editIcon.evaluate(el => (el as HTMLElement).click()); }
|
|
367
|
+
} else {
|
|
368
|
+
try { await row.click({ force: true }); }
|
|
369
|
+
catch { await row.evaluate(el => (el as HTMLElement).click()); }
|
|
370
|
+
}
|
|
371
|
+
await settle(page);
|
|
372
|
+
} else if (rowVisible) {
|
|
373
|
+
try { await row.click({ force: true }); }
|
|
374
|
+
catch { await row.evaluate(el => (el as HTMLElement).click()); }
|
|
375
|
+
await settle(page);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return rowVisible;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ─── Navigation ──────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
export async function navigateTo(page: Page, url: string) {
|
|
384
|
+
await page.goto(url);
|
|
385
|
+
await settle(page);
|
|
386
|
+
await expect(page.locator('[data-automation-id]').first()).toBeVisible({ timeout: 15_000 });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function waitForGrid(page: Page) {
|
|
390
|
+
await page.locator('.rz-datatable, .dxbl-grid, .dx-datagrid, tr.rz-data-row, tr.dx-data-row').first()
|
|
391
|
+
.waitFor({ state: 'visible', timeout: 15_000 }).catch(() => {});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Fecha Helper ────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
export function getFechas(diasAdelante = 30): { inicio: string; fin: string } {
|
|
397
|
+
const hoy = new Date();
|
|
398
|
+
const fin = new Date(hoy);
|
|
399
|
+
fin.setDate(fin.getDate() + diasAdelante);
|
|
400
|
+
|
|
401
|
+
const fmt = (d: Date) =>
|
|
402
|
+
`${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}/${d.getFullYear()}`;
|
|
403
|
+
|
|
404
|
+
return { inicio: fmt(hoy), fin: fmt(fin) };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Step Logger ─────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
export function step(n: number, desc: string) {
|
|
410
|
+
console.log(` [#${n}] ${desc}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── RUN_ID Generator ────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
export function generateRunId() {
|
|
416
|
+
return Date.now().toString().slice(-4);
|
|
417
|
+
}
|